├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── add-to-triage.yml │ ├── ci.yml │ └── release-please.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.js ├── lib ├── index.js └── visitor-keys.js ├── package.json ├── rollup.config.js ├── test-d └── index.test-d.ts ├── tests └── lib │ ├── commonjs.cjs │ ├── fixtures │ ├── bad-extends-type-reference.d.ts │ ├── bad-type-parameters.d.ts │ ├── bad-type-reference.d.ts │ ├── bad-type-value.d.ts │ ├── bad-type.d.ts │ ├── new-keys-bad.d.ts │ ├── new-keys-on-old-order-switched.d.ts │ ├── new-keys-on-old-other-order.d.ts │ ├── new-keys-on-old.d.ts │ ├── new-keys.d.ts │ └── union-omit.d.ts │ ├── get-keys-from-ts.js │ └── index.js ├── tools ├── backward-compatible-keys.js ├── build-keys-from-ts.js └── get-keys-from-ts.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | ; EditorConfig file: https://EditorConfig.org 2 | ; Install the "EditorConfig" plugin into your editor to use 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | 14 | [package.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/add-to-triage.yml: -------------------------------------------------------------------------------- 1 | name: Add to Triage 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | project-url: https://github.com/orgs/eslint/projects/3 16 | github-token: ${{ secrets.PROJECT_BOT_TOKEN }} 17 | labeled: "triage:no" 18 | label-operator: NOT 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 'lts/*' 19 | - name: Install dependencies 20 | run: npm install 21 | - name: Lint files 22 | run: npm run lint 23 | test: 24 | name: Test 25 | strategy: 26 | matrix: 27 | os: [ubuntu-latest] 28 | node: [22.x, 21.x, 20.x, 18.x, "18.18.0"] 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node }} 35 | - name: Install dependencies 36 | run: npm install 37 | - name: Run tests 38 | run: npm test 39 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | id-token: write 13 | steps: 14 | - uses: google-github-actions/release-please-action@v3 15 | id: release 16 | with: 17 | release-type: node 18 | package-name: 'eslint-visitor-keys' 19 | pull-request-title-pattern: 'chore: release ${version}' 20 | changelog-types: > 21 | [ 22 | { "type": "feat", "section": "Features", "hidden": false }, 23 | { "type": "fix", "section": "Bug Fixes", "hidden": false }, 24 | { "type": "perf", "section": "Performance Improvements", "hidden": false } 25 | ] 26 | - uses: actions/checkout@v4 27 | if: ${{ steps.release.outputs.release_created }} 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: lts/* 31 | registry-url: https://registry.npmjs.org 32 | if: ${{ steps.release.outputs.release_created }} 33 | - run: npm install 34 | if: ${{ steps.release.outputs.release_created }} 35 | - run: npm publish --provenance 36 | env: 37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | if: ${{ steps.release.outputs.release_created }} 39 | - run: 'npx @humanwhocodes/tweet "eslint-visitor-keys ${{ steps.release.outputs.tag_name }} has been released: ${{ steps.release.outputs.html_url }}"' 40 | if: ${{ steps.release.outputs.release_created }} 41 | env: 42 | TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} 43 | TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} 44 | TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} 45 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 46 | - run: 'npx @humanwhocodes/toot "eslint-visitor-keys ${{ steps.release.outputs.tag_name }} has been released: ${{ steps.release.outputs.html_url }}"' 47 | if: ${{ steps.release.outputs.release_created }} 48 | env: 49 | MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} 50 | MASTODON_HOST: ${{ secrets.MASTODON_HOST }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /.vscode 3 | /coverage 4 | /node_modules 5 | /test.* 6 | .eslint-release-info.json 7 | /dist 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.0.0](https://github.com/eslint/eslint-visitor-keys/compare/v3.4.3...v4.0.0) (2024-02-08) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * Require Node.js `^18.18.0 || ^20.9.0 || >=21.1.0` ([#63](https://github.com/eslint/eslint-visitor-keys/issues/63)) 9 | 10 | ### Features 11 | 12 | * Require Node.js `^18.18.0 || ^20.9.0 || >=21.1.0` ([#63](https://github.com/eslint/eslint-visitor-keys/issues/63)) ([388b2ac](https://github.com/eslint/eslint-visitor-keys/commit/388b2acedbe3881edd52b45f217db393731feb48)) 13 | 14 | 15 | ### Chores 16 | 17 | * run tests in Node.js 21 ([#61](https://github.com/eslint/eslint-visitor-keys/issues/61)) ([b23bfb7](https://github.com/eslint/eslint-visitor-keys/commit/b23bfb7f938d6559dfff8f02203c866a2fb26618)) 18 | 19 | ## [3.4.3](https://github.com/eslint/eslint-visitor-keys/compare/v3.4.2...v3.4.3) (2023-08-08) 20 | 21 | 22 | ### Chores 23 | 24 | * Add back add-to-triage ([#59](https://github.com/eslint/eslint-visitor-keys/issues/59)) ([5ea8b12](https://github.com/eslint/eslint-visitor-keys/commit/5ea8b120d73f1dd6db92427d025c6805df43397d)) 25 | * Remove add-to-triage ([#56](https://github.com/eslint/eslint-visitor-keys/issues/56)) ([45d4c17](https://github.com/eslint/eslint-visitor-keys/commit/45d4c17b63d26ef486c92cfb60283991e36d6db0)) 26 | * standardize npm script names ([#55](https://github.com/eslint/eslint-visitor-keys/issues/55)) ([0461695](https://github.com/eslint/eslint-visitor-keys/commit/0461695b730821c04c20d46f5cff9195509f865b)) 27 | * update `typedef` in build keys template ([#58](https://github.com/eslint/eslint-visitor-keys/issues/58)) ([eb6c66d](https://github.com/eslint/eslint-visitor-keys/commit/eb6c66dbaf6389d253d10dd74d22915d7e33d651)) 28 | 29 | ## [3.4.2](https://github.com/eslint/eslint-visitor-keys/compare/v3.4.1...v3.4.2) (2023-07-27) 30 | 31 | 32 | ### Documentation 33 | 34 | * fix spelling mistakes ([#50](https://github.com/eslint/eslint-visitor-keys/issues/50)) ([4ce1c17](https://github.com/eslint/eslint-visitor-keys/commit/4ce1c1777181b87f5dcd3f10a3d8aef0710f8d0e)) 35 | * remove `release` script reference from README ([#52](https://github.com/eslint/eslint-visitor-keys/issues/52)) ([4640146](https://github.com/eslint/eslint-visitor-keys/commit/46401465ff5bb08bf793219d399c11434fd163be)) 36 | 37 | 38 | ### Chores 39 | 40 | * Add PRs to triage ([#54](https://github.com/eslint/eslint-visitor-keys/issues/54)) ([a7b64b4](https://github.com/eslint/eslint-visitor-keys/commit/a7b64b4ea0a4548f92cb41428d3e23b30f0cf8de)) 41 | * generate provenance statements when release ([#53](https://github.com/eslint/eslint-visitor-keys/issues/53)) ([7b09698](https://github.com/eslint/eslint-visitor-keys/commit/7b09698fa51bbd9fcace50cb1014eec87abde140)) 42 | 43 | ## [3.4.1](https://github.com/eslint/eslint-visitor-keys/compare/v3.4.0...v3.4.1) (2023-05-05) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * correct types for node16 resolution ([#47](https://github.com/eslint/eslint-visitor-keys/issues/47)) ([7bd1fc1](https://github.com/eslint/eslint-visitor-keys/commit/7bd1fc1d483c2d0fdd5e0eddb2702f177372889c)) 49 | 50 | 51 | ### Chores 52 | 53 | * run tests on Node.js v20 ([#45](https://github.com/eslint/eslint-visitor-keys/issues/45)) ([c982093](https://github.com/eslint/eslint-visitor-keys/commit/c982093329f12c02dc87569930a6042f4095026b)) 54 | * set up release-please ([#48](https://github.com/eslint/eslint-visitor-keys/issues/48)) ([a7fb4c7](https://github.com/eslint/eslint-visitor-keys/commit/a7fb4c7eb5d122e89bc6c24779ea06c487242c87)) 55 | 56 | v3.4.0 - March 27, 2023 57 | 58 | * [`e9a070f`](https://github.com/eslint/eslint-visitor-keys/commit/e9a070fcbf53c14374e17801799016ce21d0c0ff) fix: remove useless sourcemap url (fixes #43) (#44) (余腾靖) 59 | * [`0398109`](https://github.com/eslint/eslint-visitor-keys/commit/0398109f1f751c58be3bd3206d22ae9c1b269219) chore: add triage action (#42) (Milos Djermanovic) 60 | * [`bcffbe5`](https://github.com/eslint/eslint-visitor-keys/commit/bcffbe52989bf726475c6b86eba3003275317f45) ci: add Node v19 (#41) (Milos Djermanovic) 61 | * [`c24f2e4`](https://github.com/eslint/eslint-visitor-keys/commit/c24f2e45cc59dbdeb8c2b48782d3599fbef9cbcb) chore: update github actions and add funding field (#40) (Deepshika S) 62 | * [`81c0732`](https://github.com/eslint/eslint-visitor-keys/commit/81c0732aa4086ad75f0adf4512823e4c8c584493) build: add node v18 (#39) (唯然) 63 | * [`6ece4bd`](https://github.com/eslint/eslint-visitor-keys/commit/6ece4bd4086965bdaf92d95b6a03d8d122468b4e) feat: add `JSXSpreadChild` and tool to build keys out of AST definitions (#36) (Brett Zamir) 64 | * [`4beb7a7`](https://github.com/eslint/eslint-visitor-keys/commit/4beb7a7be5fd7d25e5572c3dfee3e127edd8cadb) docs: update badges (#37) (Milos Djermanovic) 65 | 66 | v3.3.0 - February 11, 2022 67 | 68 | * [`7f10327`](https://github.com/eslint/eslint-visitor-keys/commit/7f103276844fb131cfad115ee78eb19f798d5fc8) feat: Bundle JSDoc-built TypeScript declaration file (#34) (Brett Zamir) 69 | 70 | v3.2.0 - January 15, 2022 71 | 72 | * [`5c53218`](https://github.com/eslint/eslint-visitor-keys/commit/5c532184e05440d3c883b3d7864f84eb1b11dc90) feat: add missing JSXOpeningFragment and JSXClosingFragment (#33) (Richard Huang) 73 | * [`2a5c9a6`](https://github.com/eslint/eslint-visitor-keys/commit/2a5c9a622d8cb09df9d40a320d146b0941081e11) docs: readme add syntax highlighting (#32) (coderaiser) 74 | 75 | v3.1.0 - November 8, 2021 76 | 77 | * [`5e3e687`](https://github.com/eslint/eslint-visitor-keys/commit/5e3e68779560a1b2edef7923d30165396bce9602) build: upgrade eslint-release to v3.2.0 to support conventional commits (#31) (Milos Djermanovic) 78 | * [`53d3939`](https://github.com/eslint/eslint-visitor-keys/commit/53d39390d3560c179cffd08638b50343b0841a30) Build: add node v17 (#30) (唯然) 79 | * [`5f5b232`](https://github.com/eslint/eslint-visitor-keys/commit/5f5b232386bd7e217dd61d08aa27c3a1e2a4665e) Update: add StaticBlock (#29) (Milos Djermanovic) 80 | * [`e89bff9`](https://github.com/eslint/eslint-visitor-keys/commit/e89bff9fd6a5929b1e8f4d5f9cedec45aa966074) Chore: use actions/setup-node@v2 (薛定谔的猫) 81 | * [`7b756cd`](https://github.com/eslint/eslint-visitor-keys/commit/7b756cd37cd28089dfee6015c001fd860e21aead) Docs: Update the minimum Node.js version requirement (#26) (0uep) 82 | 83 | v3.0.0 - June 24, 2021 84 | 85 | * [`701b67d`](https://github.com/eslint/eslint-visitor-keys/commit/701b67de7216cabebc03e7c6205fe47ce3177aa3) Breaking: drop node v10/v13/v15 (refs eslint/eslint#14023) (#23) (薛定谔的猫) 86 | * [`f584b12`](https://github.com/eslint/eslint-visitor-keys/commit/f584b121421ceb6c4e034b79943f3c32aaa0541d) Breaking: Switch to ESM (#24) (Brett Zamir) 87 | * [`7279e73`](https://github.com/eslint/eslint-visitor-keys/commit/7279e7304e95030a854408191b8fde3c01876451) Build: Update branch reference in CI (#25) (Milos Djermanovic) 88 | * [`c6846d6`](https://github.com/eslint/eslint-visitor-keys/commit/c6846d69271c73041b797b7de9c8254dcf439a2e) Upgrade: eslint-release (#21) (Nicholas C. Zakas) 89 | 90 | v2.1.0 - May 3, 2021 91 | 92 | * [`908fdf8`](https://github.com/eslint/eslint-visitor-keys/commit/908fdf8c0d9a352c696c8c1f4901280d1a0795f7) Update: add PrivateIdentifier and PropertyDefinition (#20) (Toru Nagashima) 93 | * [`2d7be11`](https://github.com/eslint/eslint-visitor-keys/commit/2d7be11e4d13ac702c9fe3c529cadbd75b370146) Chore: No longer test in Node.js 13 (#17) (Michaël De Boey) 94 | * [`b41b509`](https://github.com/eslint/eslint-visitor-keys/commit/b41b509b153ecd8d47af46a421122f64e93d4c67) Docs: Update required Node.js version (#15) (Michaël De Boey) 95 | 96 | v2.0.0 - August 14, 2020 97 | 98 | * [`fb86ca3`](https://github.com/eslint/eslint-visitor-keys/commit/fb86ca315daafc84e23ed9005db40b0892b972a6) Breaking: drop support for Node <10 (#13) (Kai Cataldo) 99 | * [`69383b3`](https://github.com/eslint/eslint-visitor-keys/commit/69383b372915e33ada094880ecc6b6e8f8c7ca4e) Chore: move to GitHub Actions (#14) (Kai Cataldo) 100 | 101 | v1.3.0 - June 19, 2020 102 | 103 | * [`c92dd7f`](https://github.com/eslint/eslint-visitor-keys/commit/c92dd7ff96f0044dba12d681406a025b92b4c437) Update: add `ChainExpression` node (#12) (Toru Nagashima) 104 | 105 | v1.2.0 - June 4, 2020 106 | 107 | * [`21f28bf`](https://github.com/eslint/eslint-visitor-keys/commit/21f28bf11be5329d740a8bf6bdbcd0ef13bbf1a2) Update: added exported in exportAllDeclaration key (#10) (Anix) 108 | 109 | v1.1.0 - August 13, 2019 110 | 111 | * [`9331cc0`](https://github.com/eslint/eslint-visitor-keys/commit/9331cc09e756e65b9044c9186445a474b037fac6) Update: add ImportExpression (#8) (Toru Nagashima) 112 | * [`5967f58`](https://github.com/eslint/eslint-visitor-keys/commit/5967f583b04f17fba9226aaa394e45d476d2b8af) Chore: add supported Node.js versions to CI (#7) (Kai Cataldo) 113 | * [`6f7c60f`](https://github.com/eslint/eslint-visitor-keys/commit/6f7c60fef2ceec9f6323202df718321cec45cab0) Upgrade: eslint-release@1.0.0 (#5) (Teddy Katz) 114 | 115 | v1.0.0 - December 18, 2017 116 | 117 | * 1f6bd38 Breaking: update keys (#4) (Toru Nagashima) 118 | 119 | v0.1.0 - November 17, 2017 120 | 121 | * 17b4a88 Chore: update `repository` field in package.json (#3) (Toru Nagashima) 122 | * a5a026b New: eslint-visitor-keys (#1) (Toru Nagashima) 123 | * a1a48b8 Update: Change license to Apache 2 (#2) (Ilya Volodin) 124 | * 2204715 Initial commit (Toru Nagashima) 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-visitor-keys 2 | 3 | [![npm version](https://img.shields.io/npm/v/eslint-visitor-keys.svg)](https://www.npmjs.com/package/eslint-visitor-keys) 4 | [![Downloads/month](https://img.shields.io/npm/dm/eslint-visitor-keys.svg)](http://www.npmtrends.com/eslint-visitor-keys) 5 | [![Build Status](https://github.com/eslint/eslint-visitor-keys/workflows/CI/badge.svg)](https://github.com/eslint/eslint-visitor-keys/actions) 6 | 7 | > [!Important] 8 | > Active development for this project has moved to https://github.com/eslint/js. Please open issues/PRs there. 9 | 10 | Constants and utilities about visitor keys to traverse AST. 11 | 12 | ## 💿 Installation 13 | 14 | Use [npm] to install. 15 | 16 | ```bash 17 | $ npm install eslint-visitor-keys 18 | ``` 19 | 20 | ### Requirements 21 | 22 | - [Node.js] `^18.18.0`, `^20.9.0`, or `>=21.1.0` 23 | 24 | 25 | ## 📖 Usage 26 | 27 | To use in an ESM file: 28 | 29 | ```js 30 | import * as evk from "eslint-visitor-keys" 31 | ``` 32 | 33 | To use in a CommonJS file: 34 | 35 | ```js 36 | const evk = require("eslint-visitor-keys") 37 | ``` 38 | 39 | ### evk.KEYS 40 | 41 | > type: `{ [type: string]: string[] | undefined }` 42 | 43 | Visitor keys. This keys are frozen. 44 | 45 | This is an object. Keys are the type of [ESTree] nodes. Their values are an array of property names which have child nodes. 46 | 47 | For example: 48 | 49 | ``` 50 | console.log(evk.KEYS.AssignmentExpression) // → ["left", "right"] 51 | ``` 52 | 53 | ### evk.getKeys(node) 54 | 55 | > type: `(node: object) => string[]` 56 | 57 | Get the visitor keys of a given AST node. 58 | 59 | This is similar to `Object.keys(node)` of ES Standard, but some keys are excluded: `parent`, `leadingComments`, `trailingComments`, and names which start with `_`. 60 | 61 | This will be used to traverse unknown nodes. 62 | 63 | For example: 64 | 65 | ```js 66 | const node = { 67 | type: "AssignmentExpression", 68 | left: { type: "Identifier", name: "foo" }, 69 | right: { type: "Literal", value: 0 } 70 | } 71 | console.log(evk.getKeys(node)) // → ["type", "left", "right"] 72 | ``` 73 | 74 | ### evk.unionWith(additionalKeys) 75 | 76 | > type: `(additionalKeys: object) => { [type: string]: string[] | undefined }` 77 | 78 | Make the union set with `evk.KEYS` and the given keys. 79 | 80 | - The order of keys is, `additionalKeys` is at first, then `evk.KEYS` is concatenated after that. 81 | - It removes duplicated keys as keeping the first one. 82 | 83 | For example: 84 | 85 | ```js 86 | console.log(evk.unionWith({ 87 | MethodDefinition: ["decorators"] 88 | })) // → { ..., MethodDefinition: ["decorators", "key", "value"], ... } 89 | ``` 90 | 91 | ## 📰 Change log 92 | 93 | See [GitHub releases](https://github.com/eslint/eslint-visitor-keys/releases). 94 | 95 | ## 🍻 Contributing 96 | 97 | Welcome. See [ESLint contribution guidelines](https://eslint.org/docs/developer-guide/contributing/). 98 | 99 | ### Development commands 100 | 101 | - `npm test` runs tests and measures code coverage. 102 | - `npm run lint` checks source codes with ESLint. 103 | - `npm run test:open-coverage` opens the code coverage report of the previous test with your default browser. 104 | 105 | 106 | [npm]: https://www.npmjs.com/ 107 | [Node.js]: https://nodejs.org/ 108 | [ESTree]: https://github.com/estree/estree 109 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintConfigESLint from "eslint-config-eslint"; 2 | import globals from "globals"; 3 | 4 | export default [ 5 | { 6 | ignores: [ 7 | "dist/", 8 | "coverage/" 9 | ] 10 | }, 11 | ...eslintConfigESLint, 12 | { 13 | linterOptions: { 14 | reportUnusedDisableDirectives: "error" 15 | }, 16 | settings: { 17 | jsdoc: { 18 | preferredTypes: { 19 | Object: "object", 20 | "object<>": "Object" 21 | } 22 | } 23 | } 24 | }, 25 | { 26 | files: ["tests/lib/**"], 27 | languageOptions: { 28 | globals: { 29 | ...globals.mocha 30 | } 31 | } 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Toru Nagashima 3 | * See LICENSE file in root directory for full license. 4 | */ 5 | import KEYS from "./visitor-keys.js"; 6 | 7 | /** 8 | * @typedef {import('./visitor-keys.js').VisitorKeys} VisitorKeys 9 | */ 10 | 11 | // List to ignore keys. 12 | const KEY_BLACKLIST = new Set([ 13 | "parent", 14 | "leadingComments", 15 | "trailingComments" 16 | ]); 17 | 18 | /** 19 | * Check whether a given key should be used or not. 20 | * @param {string} key The key to check. 21 | * @returns {boolean} `true` if the key should be used. 22 | */ 23 | function filterKey(key) { 24 | return !KEY_BLACKLIST.has(key) && key[0] !== "_"; 25 | } 26 | 27 | 28 | /* eslint-disable jsdoc/valid-types -- doesn't allow `readonly`. 29 | TODO: remove eslint-disable when https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/164 is fixed 30 | */ 31 | /** 32 | * Get visitor keys of a given node. 33 | * @param {object} node The AST node to get keys. 34 | * @returns {readonly string[]} Visitor keys of the node. 35 | */ 36 | export function getKeys(node) { 37 | return Object.keys(node).filter(filterKey); 38 | } 39 | /* eslint-enable jsdoc/valid-types -- doesn't allow `readonly` */ 40 | 41 | /** 42 | * Make the union set with `KEYS` and given keys. 43 | * @param {VisitorKeys} additionalKeys The additional keys. 44 | * @returns {VisitorKeys} The union set. 45 | */ 46 | export function unionWith(additionalKeys) { 47 | const retv = /** @type {{ [type: string]: ReadonlyArray }} */ 48 | (Object.assign({}, KEYS)); 49 | 50 | for (const type of Object.keys(additionalKeys)) { 51 | if (Object.prototype.hasOwnProperty.call(retv, type)) { 52 | const keys = new Set(additionalKeys[type]); 53 | 54 | for (const key of retv[type]) { 55 | keys.add(key); 56 | } 57 | 58 | retv[type] = Object.freeze(Array.from(keys)); 59 | } else { 60 | retv[type] = Object.freeze(Array.from(additionalKeys[type])); 61 | } 62 | } 63 | 64 | return Object.freeze(retv); 65 | } 66 | 67 | export { KEYS }; 68 | -------------------------------------------------------------------------------- /lib/visitor-keys.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/valid-types -- doesn't allow `readonly`. 2 | TODO: remove eslint-disable when https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/164 is fixed 3 | */ 4 | /** 5 | * @typedef {{ readonly [type: string]: ReadonlyArray }} VisitorKeys 6 | */ 7 | /* eslint-enable jsdoc/valid-types -- doesn't allow `readonly string[]`. TODO: check why */ 8 | 9 | /** 10 | * @type {VisitorKeys} 11 | */ 12 | const KEYS = { 13 | ArrayExpression: [ 14 | "elements" 15 | ], 16 | ArrayPattern: [ 17 | "elements" 18 | ], 19 | ArrowFunctionExpression: [ 20 | "params", 21 | "body" 22 | ], 23 | AssignmentExpression: [ 24 | "left", 25 | "right" 26 | ], 27 | AssignmentPattern: [ 28 | "left", 29 | "right" 30 | ], 31 | AwaitExpression: [ 32 | "argument" 33 | ], 34 | BinaryExpression: [ 35 | "left", 36 | "right" 37 | ], 38 | BlockStatement: [ 39 | "body" 40 | ], 41 | BreakStatement: [ 42 | "label" 43 | ], 44 | CallExpression: [ 45 | "callee", 46 | "arguments" 47 | ], 48 | CatchClause: [ 49 | "param", 50 | "body" 51 | ], 52 | ChainExpression: [ 53 | "expression" 54 | ], 55 | ClassBody: [ 56 | "body" 57 | ], 58 | ClassDeclaration: [ 59 | "id", 60 | "superClass", 61 | "body" 62 | ], 63 | ClassExpression: [ 64 | "id", 65 | "superClass", 66 | "body" 67 | ], 68 | ConditionalExpression: [ 69 | "test", 70 | "consequent", 71 | "alternate" 72 | ], 73 | ContinueStatement: [ 74 | "label" 75 | ], 76 | DebuggerStatement: [], 77 | DoWhileStatement: [ 78 | "body", 79 | "test" 80 | ], 81 | EmptyStatement: [], 82 | ExperimentalRestProperty: [ 83 | "argument" 84 | ], 85 | ExperimentalSpreadProperty: [ 86 | "argument" 87 | ], 88 | ExportAllDeclaration: [ 89 | "exported", 90 | "source" 91 | ], 92 | ExportDefaultDeclaration: [ 93 | "declaration" 94 | ], 95 | ExportNamedDeclaration: [ 96 | "declaration", 97 | "specifiers", 98 | "source" 99 | ], 100 | ExportSpecifier: [ 101 | "exported", 102 | "local" 103 | ], 104 | ExpressionStatement: [ 105 | "expression" 106 | ], 107 | ForInStatement: [ 108 | "left", 109 | "right", 110 | "body" 111 | ], 112 | ForOfStatement: [ 113 | "left", 114 | "right", 115 | "body" 116 | ], 117 | ForStatement: [ 118 | "init", 119 | "test", 120 | "update", 121 | "body" 122 | ], 123 | FunctionDeclaration: [ 124 | "id", 125 | "params", 126 | "body" 127 | ], 128 | FunctionExpression: [ 129 | "id", 130 | "params", 131 | "body" 132 | ], 133 | Identifier: [], 134 | IfStatement: [ 135 | "test", 136 | "consequent", 137 | "alternate" 138 | ], 139 | ImportDeclaration: [ 140 | "specifiers", 141 | "source" 142 | ], 143 | ImportDefaultSpecifier: [ 144 | "local" 145 | ], 146 | ImportExpression: [ 147 | "source" 148 | ], 149 | ImportNamespaceSpecifier: [ 150 | "local" 151 | ], 152 | ImportSpecifier: [ 153 | "imported", 154 | "local" 155 | ], 156 | JSXAttribute: [ 157 | "name", 158 | "value" 159 | ], 160 | JSXClosingElement: [ 161 | "name" 162 | ], 163 | JSXClosingFragment: [], 164 | JSXElement: [ 165 | "openingElement", 166 | "children", 167 | "closingElement" 168 | ], 169 | JSXEmptyExpression: [], 170 | JSXExpressionContainer: [ 171 | "expression" 172 | ], 173 | JSXFragment: [ 174 | "openingFragment", 175 | "children", 176 | "closingFragment" 177 | ], 178 | JSXIdentifier: [], 179 | JSXMemberExpression: [ 180 | "object", 181 | "property" 182 | ], 183 | JSXNamespacedName: [ 184 | "namespace", 185 | "name" 186 | ], 187 | JSXOpeningElement: [ 188 | "name", 189 | "attributes" 190 | ], 191 | JSXOpeningFragment: [], 192 | JSXSpreadAttribute: [ 193 | "argument" 194 | ], 195 | JSXSpreadChild: [ 196 | "expression" 197 | ], 198 | JSXText: [], 199 | LabeledStatement: [ 200 | "label", 201 | "body" 202 | ], 203 | Literal: [], 204 | LogicalExpression: [ 205 | "left", 206 | "right" 207 | ], 208 | MemberExpression: [ 209 | "object", 210 | "property" 211 | ], 212 | MetaProperty: [ 213 | "meta", 214 | "property" 215 | ], 216 | MethodDefinition: [ 217 | "key", 218 | "value" 219 | ], 220 | NewExpression: [ 221 | "callee", 222 | "arguments" 223 | ], 224 | ObjectExpression: [ 225 | "properties" 226 | ], 227 | ObjectPattern: [ 228 | "properties" 229 | ], 230 | PrivateIdentifier: [], 231 | Program: [ 232 | "body" 233 | ], 234 | Property: [ 235 | "key", 236 | "value" 237 | ], 238 | PropertyDefinition: [ 239 | "key", 240 | "value" 241 | ], 242 | RestElement: [ 243 | "argument" 244 | ], 245 | ReturnStatement: [ 246 | "argument" 247 | ], 248 | SequenceExpression: [ 249 | "expressions" 250 | ], 251 | SpreadElement: [ 252 | "argument" 253 | ], 254 | StaticBlock: [ 255 | "body" 256 | ], 257 | Super: [], 258 | SwitchCase: [ 259 | "test", 260 | "consequent" 261 | ], 262 | SwitchStatement: [ 263 | "discriminant", 264 | "cases" 265 | ], 266 | TaggedTemplateExpression: [ 267 | "tag", 268 | "quasi" 269 | ], 270 | TemplateElement: [], 271 | TemplateLiteral: [ 272 | "quasis", 273 | "expressions" 274 | ], 275 | ThisExpression: [], 276 | ThrowStatement: [ 277 | "argument" 278 | ], 279 | TryStatement: [ 280 | "block", 281 | "handler", 282 | "finalizer" 283 | ], 284 | UnaryExpression: [ 285 | "argument" 286 | ], 287 | UpdateExpression: [ 288 | "argument" 289 | ], 290 | VariableDeclaration: [ 291 | "declarations" 292 | ], 293 | VariableDeclarator: [ 294 | "id", 295 | "init" 296 | ], 297 | WhileStatement: [ 298 | "test", 299 | "body" 300 | ], 301 | WithStatement: [ 302 | "object", 303 | "body" 304 | ], 305 | YieldExpression: [ 306 | "argument" 307 | ] 308 | }; 309 | 310 | // Types. 311 | const NODE_TYPES = Object.keys(KEYS); 312 | 313 | // Freeze the keys. 314 | for (const type of NODE_TYPES) { 315 | Object.freeze(KEYS[type]); 316 | } 317 | Object.freeze(KEYS); 318 | 319 | export default KEYS; 320 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-visitor-keys", 3 | "version": "4.0.0", 4 | "description": "Constants and utilities about visitor keys to traverse AST.", 5 | "type": "module", 6 | "main": "dist/eslint-visitor-keys.cjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": [ 10 | { 11 | "import": "./lib/index.js", 12 | "require": "./dist/eslint-visitor-keys.cjs" 13 | }, 14 | "./dist/eslint-visitor-keys.cjs" 15 | ], 16 | "./package.json": "./package.json" 17 | }, 18 | "files": [ 19 | "dist/index.d.ts", 20 | "dist/visitor-keys.d.ts", 21 | "dist/eslint-visitor-keys.cjs", 22 | "dist/eslint-visitor-keys.d.cts", 23 | "lib" 24 | ], 25 | "engines": { 26 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 27 | }, 28 | "devDependencies": { 29 | "@types/estree": "^0.0.51", 30 | "@types/estree-jsx": "^0.0.1", 31 | "@typescript-eslint/parser": "^5.14.0", 32 | "c8": "^7.11.0", 33 | "chai": "^4.3.6", 34 | "eslint": "^8.56.0", 35 | "eslint-config-eslint": "^9.0.0", 36 | "eslint-release": "^3.2.0", 37 | "esquery": "^1.4.0", 38 | "globals": "^13.21.0", 39 | "json-diff": "^0.7.3", 40 | "mocha": "^9.2.1", 41 | "opener": "^1.5.2", 42 | "rollup": "^2.70.0", 43 | "rollup-plugin-dts": "^4.2.3", 44 | "tsd": "^0.19.1", 45 | "typescript": "^4.6.2" 46 | }, 47 | "scripts": { 48 | "build": "npm run build:cjs && npm run build:types", 49 | "build:cjs": "rollup -c", 50 | "build:debug": "npm run build:cjs -- -m && npm run build:types", 51 | "build:keys": "node tools/build-keys-from-ts", 52 | "build:types": "tsc", 53 | "lint": "eslint .", 54 | "prepare": "npm run build", 55 | "release:generate:latest": "eslint-generate-release", 56 | "release:generate:alpha": "eslint-generate-prerelease alpha", 57 | "release:generate:beta": "eslint-generate-prerelease beta", 58 | "release:generate:rc": "eslint-generate-prerelease rc", 59 | "release:publish": "eslint-publish-release", 60 | "test": "mocha tests/lib/**/*.cjs && c8 mocha tests/lib/**/*.js && npm run test:types", 61 | "test:open-coverage": "c8 report --reporter lcov && opener coverage/lcov-report/index.html", 62 | "test:types": "tsd" 63 | }, 64 | "repository": "eslint/eslint-visitor-keys", 65 | "funding": "https://opencollective.com/eslint", 66 | "keywords": [], 67 | "author": "Toru Nagashima (https://github.com/mysticatea)", 68 | "license": "Apache-2.0", 69 | "bugs": { 70 | "url": "https://github.com/eslint/eslint-visitor-keys/issues" 71 | }, 72 | "homepage": "https://github.com/eslint/eslint-visitor-keys#readme" 73 | } 74 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import dts from "rollup-plugin-dts"; 2 | 3 | export default [ 4 | { 5 | input: "./lib/index.js", 6 | treeshake: false, 7 | output: { 8 | format: "cjs", 9 | file: "dist/eslint-visitor-keys.cjs" 10 | } 11 | }, 12 | { 13 | plugins: [dts()], 14 | input: "./lib/index.js", 15 | treeshake: false, 16 | output: { 17 | format: "cjs", 18 | file: "dist/eslint-visitor-keys.d.cts" 19 | } 20 | } 21 | ]; 22 | -------------------------------------------------------------------------------- /test-d/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType, expectAssignable, expectError } from 'tsd'; 2 | 3 | import { KEYS, getKeys, unionWith, VisitorKeys } from "../"; 4 | 5 | const assignmentExpression = { 6 | type: "AssignmentExpression", 7 | operator: "=", 8 | left: { 9 | type: "Identifier", 10 | name: "a", 11 | range: [ 12 | 0, 13 | 1 14 | ] 15 | }, 16 | right: { 17 | type: "Literal", 18 | value: 5, 19 | raw: "5", 20 | range: [ 21 | 4, 22 | 5 23 | ] 24 | }, 25 | range: [ 26 | 0, 27 | 5 28 | ] 29 | }; 30 | 31 | expectType<{readonly [type: string]: readonly string[]}>(KEYS); 32 | 33 | expectType(getKeys(assignmentExpression)); 34 | 35 | expectType<{readonly [type: string]: readonly string[]}>(unionWith({ 36 | TestInterface1: ["left", "right"], 37 | TestInterface2: ["expression"] 38 | })); 39 | 40 | const readonlyKeys: { 41 | readonly [type: string]: readonly string[] 42 | } = { 43 | TestInterface1: ["left", "right"] 44 | }; 45 | 46 | expectAssignable(readonlyKeys); 47 | 48 | // https://github.com/SamVerschueren/tsd/issues/143 49 | // expectError(() => { 50 | // const erring: VisitorKeys = { 51 | // TestInterface1: ["left", "right"] 52 | // }; 53 | // erring.TestInterface1 = ["badAttemptOverwrite"]; 54 | // }); 55 | -------------------------------------------------------------------------------- /tests/lib/commonjs.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for checking that the commonjs entry points are still accessible 3 | * @author Mike Reinstein 4 | */ 5 | 6 | "use strict"; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Requirements 10 | //------------------------------------------------------------------------------ 11 | 12 | const assert = require("assert"); 13 | const eslintVisitorKeys = require("../../dist/eslint-visitor-keys.cjs"); 14 | 15 | 16 | //------------------------------------------------------------------------------ 17 | // Tests 18 | //------------------------------------------------------------------------------ 19 | 20 | describe("commonjs", () => { 21 | it("is an object", () => { 22 | assert.strictEqual(typeof eslintVisitorKeys, "object"); 23 | }); 24 | 25 | it("has exported keys object", () => { 26 | assert.strictEqual(typeof eslintVisitorKeys.KEYS, "object"); 27 | }); 28 | 29 | it("has key array with AST type", () => { 30 | assert.ok(Array.isArray(eslintVisitorKeys.KEYS.ArrayExpression)); 31 | }); 32 | 33 | it("has getKeys function", () => { 34 | assert.strictEqual(typeof eslintVisitorKeys.getKeys, "function"); 35 | }); 36 | 37 | it("should have getKeys which returns keys", () => { 38 | assert.deepStrictEqual(eslintVisitorKeys.getKeys({ a: 1, b: 2 }), ["a", "b"]); 39 | }); 40 | 41 | it("has unionWith function", () => { 42 | assert.strictEqual(typeof eslintVisitorKeys.unionWith, "function"); 43 | }); 44 | 45 | it("should have unionWith which includes all additional keys", () => { 46 | const additionalKeys = { Program: ["body", "a"], AssignmentExpression: ["b"], additional: ["c"], MethodDefinition: ["a", "key", "b"] }; 47 | const unionKeys = eslintVisitorKeys.unionWith(additionalKeys); 48 | 49 | for (const type of Object.keys(additionalKeys)) { 50 | for (const key of additionalKeys[type]) { 51 | assert(unionKeys[type].includes(key), `'${key}' should be included in '${type}'.`); 52 | } 53 | } 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/lib/fixtures/bad-extends-type-reference.d.ts: -------------------------------------------------------------------------------- 1 | export interface Something extends BadSomething { 2 | type: "Something"; 3 | } 4 | -------------------------------------------------------------------------------- /tests/lib/fixtures/bad-type-parameters.d.ts: -------------------------------------------------------------------------------- 1 | export interface Statement {} 2 | 3 | export interface StaticBlock extends BadTypeParam { 4 | type: "StaticBlock"; 5 | } 6 | -------------------------------------------------------------------------------- /tests/lib/fixtures/bad-type-reference.d.ts: -------------------------------------------------------------------------------- 1 | export interface StaticBlock extends Omit { 2 | type: "StaticBlock"; 3 | } 4 | -------------------------------------------------------------------------------- /tests/lib/fixtures/bad-type-value.d.ts: -------------------------------------------------------------------------------- 1 | interface BadExpression { 2 | type: undefined; 3 | } 4 | 5 | export interface NewFangledExpression { 6 | type: "NewFangledExpression"; 7 | right: BadExpression; 8 | } 9 | -------------------------------------------------------------------------------- /tests/lib/fixtures/bad-type.d.ts: -------------------------------------------------------------------------------- 1 | export interface SomeExpression { 2 | type: "SomeExpression"; 3 | someProperty: any; 4 | } 5 | -------------------------------------------------------------------------------- /tests/lib/fixtures/new-keys-bad.d.ts: -------------------------------------------------------------------------------- 1 | export interface NewFangledExpression { 2 | type: "NewFangledExpression"; 3 | right: BadExpression; 4 | } 5 | -------------------------------------------------------------------------------- /tests/lib/fixtures/new-keys-on-old-order-switched.d.ts: -------------------------------------------------------------------------------- 1 | export type AssignmentOperator = "="; 2 | interface Pattern { 3 | type: "Pattern" 4 | }; 5 | interface MemberExpression { 6 | type: "MemberExpression" 7 | }; 8 | interface Expression { 9 | type: "Expression" 10 | }; 11 | 12 | export interface AssignmentExpression { 13 | type: "AssignmentExpression"; 14 | operator: AssignmentOperator; 15 | down: Expression; 16 | up: Expression; 17 | left: Pattern | MemberExpression; 18 | right: Expression; 19 | nontraversable: RegExp; 20 | } 21 | -------------------------------------------------------------------------------- /tests/lib/fixtures/new-keys-on-old-other-order.d.ts: -------------------------------------------------------------------------------- 1 | export type AssignmentOperator = "="; 2 | interface Pattern { 3 | type: "Pattern" 4 | }; 5 | interface MemberExpression { 6 | type: "MemberExpression" 7 | }; 8 | interface Expression { 9 | type: "Expression" 10 | }; 11 | 12 | export interface AssignmentExpression { 13 | type: "AssignmentExpression"; 14 | operator: AssignmentOperator; 15 | up: Expression; 16 | left: Pattern | MemberExpression; 17 | down: Expression; 18 | right: Expression; 19 | nontraversable: RegExp; 20 | } 21 | -------------------------------------------------------------------------------- /tests/lib/fixtures/new-keys-on-old.d.ts: -------------------------------------------------------------------------------- 1 | export type AssignmentOperator = "="; 2 | 3 | interface IgnoreBase { 4 | type: "Line"; 5 | } 6 | 7 | type AnotherIgnore = IgnoreBase; 8 | 9 | interface BasePattern { 10 | type: "Pattern" 11 | }; 12 | interface IgnoreChild extends Omit { 13 | }; 14 | 15 | interface Pattern { 16 | type: "Pattern" 17 | }; 18 | interface MemberExpression { 19 | type: "MemberExpression" 20 | }; 21 | interface Expression { 22 | type: "Expression" 23 | }; 24 | 25 | export interface AssignmentExpression { 26 | type: "AssignmentExpression"; 27 | ignore: IgnoreChild; 28 | anotherIgnore: AnotherIgnore; 29 | operator: AssignmentOperator; 30 | up: Expression; 31 | down: Expression; 32 | left: Pattern | MemberExpression; 33 | right: Expression; 34 | nontraversable: RegExp; 35 | } 36 | -------------------------------------------------------------------------------- /tests/lib/fixtures/new-keys.d.ts: -------------------------------------------------------------------------------- 1 | export type AssignmentOperator = "="; 2 | interface Pattern { 3 | type: "Pattern" 4 | }; 5 | interface MemberExpression { 6 | type: "MemberExpression" 7 | }; 8 | interface Expression { 9 | type: "Expression" 10 | }; 11 | 12 | export interface NewFangledExpression { 13 | type: "NewFangledExpression"; 14 | operator: AssignmentOperator; 15 | up: Expression; 16 | down: Expression; 17 | left: Pattern | MemberExpression; 18 | right: Expression; 19 | } 20 | -------------------------------------------------------------------------------- /tests/lib/fixtures/union-omit.d.ts: -------------------------------------------------------------------------------- 1 | export interface IgnoredStatement { 2 | type: "IgnoredStatement" 3 | } 4 | export interface AnotherStatement { 5 | type: "AnotherStatement"; 6 | anotherToIgnore: IgnoredStatement; 7 | } 8 | 9 | export interface StaticBlock extends Omit { 10 | type: "StaticBlock"; 11 | } 12 | -------------------------------------------------------------------------------- /tests/lib/get-keys-from-ts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for checking that our build tool can retrieve keys out of TypeScript AST. 3 | * @author Brett Zamir 4 | */ 5 | 6 | import { diffString } from "json-diff"; 7 | import { expect } from "chai"; 8 | import { alphabetizeKeyInterfaces, getKeysFromTsFile } from "../../tools/get-keys-from-ts.js"; 9 | import { KEYS } from "../../lib/index.js"; 10 | import backwardCompatibleKeys from "../../tools/backward-compatible-keys.js"; 11 | 12 | describe("getKeysFromTsFile", () => { 13 | it("gets keys", async () => { 14 | const { keys, tsInterfaceDeclarations } = await getKeysFromTsFile( 15 | "./node_modules/@types/estree/index.d.ts" 16 | ); 17 | const { keys: jsxKeys } = await getKeysFromTsFile( 18 | "./node_modules/@types/estree-jsx/index.d.ts", 19 | { 20 | supplementaryDeclarations: tsInterfaceDeclarations 21 | } 22 | ); 23 | 24 | const actual = alphabetizeKeyInterfaces({ ...keys, ...jsxKeys, ...backwardCompatibleKeys }); 25 | 26 | const expected = KEYS; 27 | 28 | // eslint-disable-next-line no-console -- Mocha's may drop diffs so show with json-diff 29 | console.log("JSON Diffs:", diffString(actual, expected) || "(none)"); 30 | 31 | expect(actual).to.deep.equal(expected); 32 | }); 33 | 34 | it("gets keys minus explicitly omitted ones", async () => { 35 | const { keys: actual } = await getKeysFromTsFile( 36 | "./tests/lib/fixtures/union-omit.d.ts" 37 | ); 38 | 39 | const expected = { 40 | AnotherStatement: [ 41 | "anotherToIgnore" 42 | ], 43 | IgnoredStatement: [], 44 | StaticBlock: [] 45 | }; 46 | 47 | expect(actual).to.deep.equal(expected); 48 | }); 49 | 50 | it("sorts keys alphabetically if new", async () => { 51 | const { keys: actual } = await getKeysFromTsFile( 52 | "./tests/lib/fixtures/new-keys.d.ts" 53 | ); 54 | 55 | const expected = { 56 | NewFangledExpression: [ 57 | "down", 58 | "left", 59 | "right", 60 | "up" 61 | ] 62 | }; 63 | 64 | expect(actual).to.deep.equal(expected); 65 | }); 66 | 67 | it("sorts extra keys at end alphabetically", async () => { 68 | const { keys: actual } = await getKeysFromTsFile( 69 | "./tests/lib/fixtures/new-keys-on-old.d.ts" 70 | ); 71 | 72 | const expected = { 73 | AssignmentExpression: [ 74 | "left", 75 | "right", 76 | "down", 77 | "up" 78 | ] 79 | }; 80 | 81 | expect(actual).to.deep.equal(expected); 82 | }); 83 | 84 | it("sorts extra keys at end alphabetically (other order)", async () => { 85 | const { keys: actual } = await getKeysFromTsFile( 86 | "./tests/lib/fixtures/new-keys-on-old-other-order.d.ts" 87 | ); 88 | 89 | const expected = { 90 | AssignmentExpression: [ 91 | "left", 92 | "right", 93 | "down", 94 | "up" 95 | ] 96 | }; 97 | 98 | expect(actual).to.deep.equal(expected); 99 | }); 100 | 101 | it("sorts extra keys at end alphabetically (switched order)", async () => { 102 | const { keys: actual } = await getKeysFromTsFile( 103 | "./tests/lib/fixtures/new-keys-on-old-order-switched.d.ts" 104 | ); 105 | 106 | const expected = { 107 | AssignmentExpression: [ 108 | "left", 109 | "right", 110 | "down", 111 | "up" 112 | ] 113 | }; 114 | 115 | expect(actual).to.deep.equal(expected); 116 | }); 117 | 118 | it("throws with unhandled TS type reference", async () => { 119 | let error; 120 | 121 | try { 122 | await getKeysFromTsFile( 123 | "./tests/lib/fixtures/bad-type-reference.d.ts" 124 | ); 125 | } catch (err) { 126 | error = err; 127 | } 128 | 129 | expect(error.message).to.contain("Unhandled TypeScript type reference"); 130 | }); 131 | 132 | it("throws with unhandled extends TS type reference", async () => { 133 | let error; 134 | 135 | try { 136 | await getKeysFromTsFile( 137 | "./tests/lib/fixtures/bad-extends-type-reference.d.ts" 138 | ); 139 | } catch (err) { 140 | error = err; 141 | } 142 | 143 | expect(error.message).to.contain("Unhandled TypeScript type reference"); 144 | }); 145 | 146 | it("throws with unhandled TS type", async () => { 147 | let error; 148 | 149 | try { 150 | await getKeysFromTsFile( 151 | "./tests/lib/fixtures/bad-type.d.ts" 152 | ); 153 | } catch (err) { 154 | error = err; 155 | } 156 | 157 | expect(error.message).to.contain("Unhandled TypeScript type;"); 158 | }); 159 | 160 | it("throws with unhandled TS typeParameters", async () => { 161 | let error; 162 | 163 | try { 164 | await getKeysFromTsFile( 165 | "./tests/lib/fixtures/bad-type-parameters.d.ts" 166 | ); 167 | } catch (err) { 168 | error = err; 169 | } 170 | 171 | expect(error.message).to.contain("Unknown type parameter"); 172 | }); 173 | 174 | it("throws with bad key", async () => { 175 | let error; 176 | 177 | try { 178 | await getKeysFromTsFile( 179 | "./tests/lib/fixtures/new-keys-bad.d.ts" 180 | ); 181 | } catch (err) { 182 | error = err; 183 | } 184 | 185 | expect(error.message).to.equal("Type unknown as to traversability: BadExpression"); 186 | }); 187 | 188 | it("throws with bad type value", async () => { 189 | let error; 190 | 191 | try { 192 | await getKeysFromTsFile( 193 | "./tests/lib/fixtures/bad-type-value.d.ts" 194 | ); 195 | } catch (err) { 196 | error = err; 197 | } 198 | 199 | expect(error.message).to.equal("Unexpected `type` value property type TSUndefinedKeyword"); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /tests/lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Toru Nagashima 3 | * See LICENSE file in root directory for full license. 4 | */ 5 | import assert from "assert"; 6 | import * as evk from "../../lib/index.js"; 7 | import keys from "../../lib/visitor-keys.js"; 8 | 9 | describe("eslint-visitor-keys", () => { 10 | describe("KEYS", () => { 11 | it("should be same as lib/visitor-keys.js", () => { 12 | assert.deepStrictEqual(evk.KEYS, keys); 13 | }); 14 | }); 15 | 16 | describe("getKeys()", () => { 17 | it("should return keys", () => { 18 | assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2 }), ["a", "b"]); 19 | }); 20 | 21 | it("should not include 'parent' in the result", () => { 22 | assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, parent: 3 }), ["a", "b"]); 23 | }); 24 | 25 | it("should not include 'leadingComments' in the result", () => { 26 | assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, leadingComments: 3 }), ["a", "b"]); 27 | }); 28 | 29 | it("should not include 'trailingComments' in the result", () => { 30 | assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, trailingComments: 3 }), ["a", "b"]); 31 | }); 32 | 33 | it("should not include '_foo' in the result", () => { 34 | assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, _foo: 3 }), ["a", "b"]); 35 | }); 36 | }); 37 | 38 | describe("unionWith()", () => { 39 | const additionalKeys = { Program: ["body", "a"], AssignmentExpression: ["b"], additional: ["c"], MethodDefinition: ["a", "key", "b"] }; 40 | const unionKeys = evk.unionWith(additionalKeys); 41 | 42 | it("should include all keys of lib/visitor-keys.js", () => { 43 | for (const type of Object.keys(keys)) { 44 | for (const key of keys[type]) { 45 | assert(unionKeys[type].includes(key), `'${key}' should be included in '${type}'.`); 46 | } 47 | } 48 | }); 49 | 50 | it("should include all additional keys", () => { 51 | for (const type of Object.keys(additionalKeys)) { 52 | for (const key of additionalKeys[type]) { 53 | assert(unionKeys[type].includes(key), `'${key}' should be included in '${type}'.`); 54 | } 55 | } 56 | }); 57 | 58 | it("should not have duplicate", () => { 59 | assert(unionKeys.Program.filter(key => key === "body").length === 1); 60 | }); 61 | 62 | it("should add additional keys, then concatenate original keys", () => { 63 | assert.deepStrictEqual(unionKeys.MethodDefinition, ["a", "key", "b", "value"]); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tools/backward-compatible-keys.js: -------------------------------------------------------------------------------- 1 | const backwardCompatibleKeys = { 2 | ExperimentalRestProperty: [ 3 | "argument" 4 | ], 5 | ExperimentalSpreadProperty: [ 6 | "argument" 7 | ] 8 | }; 9 | 10 | export default backwardCompatibleKeys; 11 | -------------------------------------------------------------------------------- /tools/build-keys-from-ts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Script to build our visitor keys based on TypeScript AST. 3 | * 4 | * Uses `get-keys-from-ts.js` to read the files and build the keys and then 5 | * merges them in alphabetical order of Node type before writing to file. 6 | * 7 | * @author Brett Zamir 8 | */ 9 | 10 | import fs from "fs"; 11 | import { alphabetizeKeyInterfaces, getKeysFromTsFile } from "./get-keys-from-ts.js"; 12 | import backwardCompatibleKeys from "./backward-compatible-keys.js"; 13 | 14 | const { promises: { writeFile } } = fs; 15 | 16 | (async () => { 17 | const { keys, tsInterfaceDeclarations } = await getKeysFromTsFile("./node_modules/@types/estree/index.d.ts"); 18 | const { keys: jsxKeys } = await getKeysFromTsFile( 19 | "./node_modules/@types/estree-jsx/index.d.ts", 20 | { 21 | supplementaryDeclarations: tsInterfaceDeclarations 22 | } 23 | ); 24 | 25 | const mergedKeys = alphabetizeKeyInterfaces({ ...keys, ...jsxKeys, ...backwardCompatibleKeys }); 26 | 27 | // eslint-disable-next-line no-console -- CLI 28 | console.log("keys", mergedKeys); 29 | 30 | writeFile( 31 | "./lib/visitor-keys.js", 32 | // eslint-disable-next-line indent -- Readability 33 | `/** 34 | * @typedef {{ readonly [type: string]: ReadonlyArray }} VisitorKeys 35 | */ 36 | 37 | /** 38 | * @type {VisitorKeys} 39 | */ 40 | const KEYS = ${JSON.stringify(mergedKeys, null, 4).replace(/"(.*?)":/gu, "$1:")}; 41 | 42 | // Types. 43 | const NODE_TYPES = Object.keys(KEYS); 44 | 45 | // Freeze the keys. 46 | for (const type of NODE_TYPES) { 47 | Object.freeze(KEYS[type]); 48 | } 49 | Object.freeze(KEYS); 50 | 51 | export default KEYS; 52 | ` 53 | ); 54 | 55 | })(); 56 | -------------------------------------------------------------------------------- /tools/get-keys-from-ts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Script to build our visitor keys based on TypeScript AST. 3 | * 4 | * Uses `get-keys-from-ts.js` to read the files and build the keys and then 5 | * merges them in alphabetical order of Node type before writing to file. 6 | * 7 | * @author Brett Zamir 8 | */ 9 | 10 | //------------------------------------------------------------------------------ 11 | // Requirements 12 | //------------------------------------------------------------------------------ 13 | 14 | import { promises } from "fs"; 15 | import { parseForESLint } from "@typescript-eslint/parser"; 16 | import esquery from "esquery"; 17 | 18 | import { getKeys, KEYS } from "../lib/index.js"; 19 | 20 | const { readFile } = promises; 21 | 22 | //------------------------------------------------------------------------------ 23 | // Helpers 24 | //------------------------------------------------------------------------------ 25 | 26 | const knownTypes = new Set([ 27 | "TSUndefinedKeyword", 28 | "TSNullKeyword", 29 | "TSUnknownKeyword", 30 | "TSBooleanKeyword", 31 | "TSNumberKeyword", 32 | "TSStringKeyword", 33 | "TSLiteralType", // E.g., `true` 34 | 35 | // Apparently used for primitives, so exempting 36 | "TSTypeLiteral", // E.g., `{value: {cooked, raw}}` 37 | 38 | "TSUnionType", // I.e., `|` 39 | "TSTypeReference" 40 | ]); 41 | 42 | const notTraversableTypes = new Set([ 43 | "RegExp" 44 | ]); 45 | 46 | const notTraversableTSTypes = new Set([ 47 | "TSUndefinedKeyword", 48 | "TSNullKeyword", 49 | "TSBooleanKeyword", 50 | "TSNumberKeyword", 51 | "TSStringKeyword", 52 | "TSBigIntKeyword", 53 | "TSLiteralType" 54 | ]); 55 | 56 | const commentTypes = new Set([ 57 | "Line", 58 | "Block" 59 | ]); 60 | 61 | /** 62 | * Get the literal names out of AST 63 | * @param {Node} excludedItem Excluded node 64 | * @returns {string[]} The literal names 65 | */ 66 | function findOmitTypes(excludedItem) { 67 | if (excludedItem.type === "TSUnionType") { 68 | return excludedItem.types.map(typeNode => findOmitTypes(typeNode)); 69 | } 70 | return excludedItem.literal.value; 71 | } 72 | 73 | /** 74 | * Checks whether property should be excluded 75 | * @param {string} property Property to check 76 | * @param {string[]} excludedProperties Properties not to allow 77 | * @returns {boolean} Whether or not to be excluded 78 | */ 79 | function isPropertyExcluded(property, excludedProperties) { 80 | return excludedProperties && excludedProperties.includes(property); 81 | } 82 | 83 | //------------------------------------------------------------------------------ 84 | // Public APIs 85 | //------------------------------------------------------------------------------ 86 | 87 | /** 88 | * Returns alphabetized keys 89 | * @param {KeysStrict} initialNodes Initial node list to sort 90 | * @returns {KeysStrict} The keys 91 | */ 92 | function alphabetizeKeyInterfaces(initialNodes) { 93 | 94 | /** 95 | * Alphabetize 96 | * @param {string} typeA The first type to compare 97 | * @param {string} typeB The second type to compare 98 | * @returns {1|-1} The sorting index 99 | */ 100 | function alphabetize([typeA], [typeB]) { 101 | return typeA < typeB ? -1 : 1; 102 | } 103 | const sortedNodeEntries = Object.entries(initialNodes).sort(alphabetize); 104 | 105 | /** 106 | * Get the key sorter for a given type 107 | * @param {string} type The type 108 | * @returns {(string, string) => -1|1} The sorter 109 | */ 110 | function getKeySorter(type) { 111 | const sequence = KEYS[type]; 112 | 113 | /** 114 | * Alphabetize 115 | * @param {string} typeA The first type to compare 116 | * @param {string} typeB The second type to compare 117 | * @returns {1|-1} The sorting index 118 | */ 119 | return function sortKeys(typeA, typeB) { 120 | if (!sequence) { 121 | return typeA < typeB ? -1 : 1; 122 | } 123 | 124 | const idxA = sequence.indexOf(typeA); 125 | const idxB = sequence.indexOf(typeB); 126 | 127 | if (idxA === -1 && idxB === -1) { 128 | return typeA < typeB ? -1 : 1; 129 | } 130 | if (idxA === -1) { 131 | return 1; 132 | } 133 | if (idxB === -1) { 134 | return -1; 135 | } 136 | 137 | return idxA < idxB ? -1 : 1; 138 | }; 139 | } 140 | 141 | for (const [type, keys] of sortedNodeEntries) { 142 | keys.sort(getKeySorter(type)); 143 | } 144 | 145 | return Object.fromEntries(sortedNodeEntries); 146 | } 147 | 148 | /** 149 | * Traverse interface `extends` 150 | * @param {Node} declNode The TS declaration node 151 | * @param {Function} handler The callback 152 | * @returns {any[]} Return value of handler 153 | * @throws {Error} If it finds an unknown type parameter. 154 | */ 155 | function traverseExtends(declNode, handler) { 156 | const ret = []; 157 | 158 | for (const extension of declNode.extends || []) { 159 | const { typeParameters, expression } = extension; 160 | const innerInterfaceName = expression.name; 161 | 162 | let res; 163 | 164 | if (typeParameters) { 165 | if (innerInterfaceName !== "Omit") { 166 | throw new Error("Unknown type parameter"); 167 | } 168 | 169 | const [param, ...excludedAST] = typeParameters.params; 170 | const paramInterfaceName = param.typeName.name; 171 | const excluded = excludedAST.flatMap(findOmitTypes); 172 | 173 | res = handler({ iName: paramInterfaceName, excluded }); 174 | } else { 175 | res = handler({ iName: innerInterfaceName }); 176 | } 177 | 178 | ret.push(res); 179 | } 180 | 181 | return ret; 182 | } 183 | 184 | /** 185 | * Traverse the properties of a declaration node. 186 | * @param {Node} tsDeclarationNode The declaration node 187 | * @param {(string) => void} handler Passed the property 188 | * @returns {any[]} The return values of the callback 189 | */ 190 | function traverseProperties(tsDeclarationNode, handler) { 191 | const tsPropertySignatures = tsDeclarationNode.body.body; 192 | 193 | const ret = []; 194 | 195 | for (const tsPropertySignature of tsPropertySignatures) { 196 | const property = tsPropertySignature.key.name; 197 | 198 | const tsAnnotation = tsPropertySignature.typeAnnotation.typeAnnotation; 199 | 200 | const res = handler({ property, tsAnnotation }); 201 | 202 | ret.push(res); 203 | } 204 | 205 | return ret; 206 | } 207 | 208 | /** 209 | * Builds visitor keys based on TypeScript declaration. 210 | * @param {string} code TypeScript declaration file as code to parse. 211 | * @param {{supplementaryDeclarations: Node[]}} [options] The options 212 | * @returns {VisitorKeysExport} The built visitor keys 213 | * @throws {Error} If it finds an unknown type. 214 | */ 215 | function getKeysFromTs(code, { 216 | 217 | // Todo: Ideally we'd just get these from the import 218 | supplementaryDeclarations = { 219 | allTsInterfaceDeclarations: [], 220 | exportedTsInterfaceDeclarations: [], 221 | tsTypeDeclarations: [] 222 | } 223 | } = {}) { 224 | const unrecognizedTSTypeReferences = new Set(); 225 | const unrecognizedTSTypes = new Set(); 226 | 227 | const parsedTSDeclaration = parseForESLint(code); 228 | 229 | const allTsInterfaceDeclarations = [...esquery.query( 230 | parsedTSDeclaration.ast, 231 | "TSInterfaceDeclaration", 232 | { 233 | 234 | // TypeScript keys here to find our *.d.ts nodes (not for the ESTree 235 | // ones we want) 236 | visitorKeys: parsedTSDeclaration.visitorKeys 237 | } 238 | ), ...supplementaryDeclarations.allTsInterfaceDeclarations]; 239 | 240 | const exportedTsInterfaceDeclarations = [...esquery.query( 241 | parsedTSDeclaration.ast, 242 | "ExportNamedDeclaration > TSInterfaceDeclaration", 243 | { 244 | 245 | // TypeScript keys here to find our *.d.ts nodes (not for the ESTree 246 | // ones we want) 247 | visitorKeys: parsedTSDeclaration.visitorKeys 248 | } 249 | ), ...supplementaryDeclarations.exportedTsInterfaceDeclarations]; 250 | 251 | const tsTypeDeclarations = [...esquery.query( 252 | parsedTSDeclaration.ast, 253 | "TSTypeAliasDeclaration", 254 | { 255 | 256 | // TypeScript keys here to find our *.d.ts nodes (not for the ESTree 257 | // ones we want) 258 | visitorKeys: parsedTSDeclaration.visitorKeys 259 | } 260 | ), ...supplementaryDeclarations.tsTypeDeclarations]; 261 | 262 | const initialNodes = {}; 263 | 264 | /** 265 | * Finds a TypeScript interface declaration. 266 | * @param {string} interfaceName The type name. 267 | * @returns {Node} The interface declaration node 268 | */ 269 | function findTsInterfaceDeclaration(interfaceName) { 270 | return allTsInterfaceDeclarations.find( 271 | innerTsDeclaration => innerTsDeclaration.id.name === interfaceName 272 | ); 273 | } 274 | 275 | /** 276 | * Finds a TypeScript type declaration. 277 | * @param {string} typeName A type name 278 | * @returns {Node} The type declaration node 279 | */ 280 | function findTsTypeDeclaration(typeName) { 281 | return tsTypeDeclarations.find(typeDecl => typeDecl.id.name === typeName); 282 | } 283 | 284 | /** 285 | * Whether has a valid (non-comment) type 286 | * @param {object} cfg Config object 287 | * @param {string} cfg.property The property name 288 | * @param {Node} cfg.tsAnnotation The annotation node 289 | * @returns {boolean} Whether has a traverseable type 290 | * @throws {Error} If it finds an unknown type. 291 | */ 292 | function hasValidType({ property, tsAnnotation }) { 293 | const tsPropertyType = tsAnnotation.type; 294 | 295 | if (property !== "type") { 296 | return false; 297 | } 298 | 299 | switch (tsPropertyType) { 300 | case "TSLiteralType": 301 | return typeof tsAnnotation.literal.value === "string" && 302 | !commentTypes.has(tsAnnotation.literal.value); 303 | case "TSStringKeyword": 304 | 305 | // Ok, but not sufficient 306 | return false; 307 | case "TSUnionType": 308 | return tsAnnotation.types.some(annType => hasValidType({ 309 | property: "type", 310 | tsAnnotation: annType 311 | })); 312 | default: 313 | throw new Error(`Unexpected \`type\` value property type ${tsPropertyType}`); 314 | } 315 | } 316 | 317 | /** 318 | * Whether the interface has a valid type ancestor 319 | * @param {string} interfaceName The interface to check 320 | * @returns {void} 321 | * @throws {Error} If it finds an unknown type. 322 | */ 323 | function hasValidTypeAncestor(interfaceName) { 324 | let decl = findTsInterfaceDeclaration(interfaceName); 325 | 326 | if (decl) { 327 | if (traverseProperties(decl, hasValidType).some(hasValid => hasValid)) { 328 | return true; 329 | } 330 | } 331 | 332 | if (!decl) { 333 | decl = findTsTypeDeclaration(interfaceName); 334 | if (decl) { 335 | if (!decl.typeAnnotation.types) { 336 | return notTraversableTSTypes.has(decl.typeAnnotation.type) 337 | ? false 338 | : hasValidTypeAncestor(decl.typeAnnotation.typeName.name); 339 | } 340 | 341 | return decl.typeAnnotation.types.some(type => { 342 | if (!type.typeName) { 343 | 344 | // Literal 345 | return false; 346 | } 347 | 348 | return hasValidTypeAncestor(type.typeName.name); 349 | }); 350 | } 351 | } 352 | 353 | if (!decl) { 354 | throw new Error(`Type unknown as to traversability: ${interfaceName}`); 355 | } 356 | 357 | if (traverseExtends(decl, ({ iName, excluded }) => { 358 | 359 | // We don't want to look at this ancestor's `type` if being excluded 360 | if (excluded && excluded.includes("type")) { 361 | return false; 362 | } 363 | 364 | return hasValidTypeAncestor(iName); 365 | }).some(hasValid => hasValid)) { 366 | return true; 367 | } 368 | 369 | return false; 370 | } 371 | 372 | /** 373 | * Determine whether the Node is traversable 374 | * @param {Node} annotationType The annotation type Node 375 | * @param {string} property The property name 376 | * @returns {boolean} Whether the node is traversable 377 | */ 378 | function checkTraversability(annotationType, property) { 379 | if ( 380 | notTraversableTSTypes.has(annotationType.type) 381 | ) { 382 | return false; 383 | } 384 | 385 | if (annotationType.type === "TSTupleType") { 386 | return annotationType.elementTypes.some(annType => checkTraversability(annType, property)); 387 | } 388 | 389 | if (annotationType.type === "TSUnionType") { 390 | return annotationType.types.some(annType => checkTraversability(annType, property)); 391 | } 392 | 393 | if (annotationType.typeName.name === "Array") { 394 | return annotationType.typeParameters.params.some(annType => checkTraversability(annType, property)); 395 | } 396 | 397 | if ( 398 | notTraversableTypes.has(annotationType.typeName.name) 399 | ) { 400 | return false; 401 | } 402 | 403 | if (hasValidTypeAncestor(annotationType.typeName.name)) { 404 | return true; 405 | } 406 | 407 | return false; 408 | } 409 | 410 | /** 411 | * Adds a property to a node based on a type declaration node's contents. 412 | * @param {Node} tsDeclarationNode TypeScript declaration node 413 | * @param {Node} node The Node on which to build 414 | * @param {string[]} excludedProperties Excluded properties 415 | * @returns {void} 416 | */ 417 | function addPropertyToNodeForDeclaration(tsDeclarationNode, node, excludedProperties) { 418 | 419 | traverseProperties(tsDeclarationNode, ({ property, tsAnnotation }) => { 420 | if (isPropertyExcluded(property, excludedProperties)) { 421 | return; 422 | } 423 | 424 | const tsPropertyType = tsAnnotation.type; 425 | 426 | if (property === "type" && tsPropertyType === "TSLiteralType") { 427 | 428 | // console.log('tsAnnotation', tsAnnotation); 429 | // node[property] = tsAnnotation.literal.value; 430 | // return; 431 | } 432 | 433 | // For sanity-checking 434 | if (!knownTypes.has(tsPropertyType)) { 435 | unrecognizedTSTypes.add(tsPropertyType); 436 | return; 437 | } 438 | 439 | switch (tsPropertyType) { 440 | case "TSUnionType": 441 | if (tsAnnotation.types.some(annType => checkTraversability(annType, property))) { 442 | break; 443 | } 444 | return; 445 | case "TSTypeReference": { 446 | if (checkTraversability(tsAnnotation, property)) { 447 | break; 448 | } 449 | 450 | return; 451 | } default: 452 | return; 453 | } 454 | 455 | node[property] = null; 456 | }); 457 | 458 | traverseExtends(tsDeclarationNode, ({ iName, excluded }) => { 459 | const innerTsDeclarationNode = findTsInterfaceDeclaration(iName); 460 | 461 | if (!innerTsDeclarationNode) { 462 | unrecognizedTSTypeReferences.add(iName); 463 | return; 464 | } 465 | 466 | addPropertyToNodeForDeclaration(innerTsDeclarationNode, node, excluded); 467 | }); 468 | } 469 | 470 | for (const tsDeclarationNode of exportedTsInterfaceDeclarations) { 471 | const bodyType = tsDeclarationNode.body.body.find( 472 | prop => prop.key.name === "type" 473 | ); 474 | 475 | const typeName = bodyType && bodyType.typeAnnotation && 476 | bodyType.typeAnnotation.typeAnnotation && 477 | bodyType.typeAnnotation.typeAnnotation.literal && 478 | bodyType.typeAnnotation.typeAnnotation.literal.value; 479 | 480 | if (!typeName) { 481 | continue; 482 | } 483 | 484 | const node = {}; 485 | 486 | addPropertyToNodeForDeclaration(tsDeclarationNode, node); 487 | 488 | initialNodes[typeName] = [...new Set(getKeys(node), ...(initialNodes[typeName] || []))]; 489 | } 490 | 491 | const nodes = alphabetizeKeyInterfaces(initialNodes); 492 | 493 | if (unrecognizedTSTypes.size) { 494 | throw new Error( 495 | "Unhandled TypeScript type; please update the code to " + 496 | "handle the type or if not relevant, add it to " + 497 | "`unrecognizedTSTypes`; see\n\n " + 498 | `${[...unrecognizedTSTypes].join(", ")}\n` 499 | ); 500 | } 501 | if (unrecognizedTSTypeReferences.size) { 502 | throw new Error( 503 | "Unhandled TypeScript type reference; please update the code to " + 504 | "handle the type reference or if not relevant, add it to " + 505 | "`unrecognizedTSTypeReferences`; see\n\n " + 506 | `${[...unrecognizedTSTypeReferences].join(", ")}\n` 507 | ); 508 | } 509 | 510 | return { 511 | keys: nodes, 512 | tsInterfaceDeclarations: { 513 | allTsInterfaceDeclarations, 514 | exportedTsInterfaceDeclarations, 515 | tsTypeDeclarations 516 | } 517 | }; 518 | } 519 | 520 | /** 521 | * @typedef {{ 522 | * keys: KeysStrict, 523 | * tsInterfaceDeclarations: { 524 | * allTsInterfaceDeclarations: Node[], 525 | * exportedTsInterfaceDeclarations: Node[] 526 | * } 527 | * }} VisitorKeysExport 528 | */ 529 | 530 | /** 531 | * Builds visitor keys based on TypeScript declaration. 532 | * @param {string} file TypeScript declaration file to parse. 533 | * @param {{supplementaryDeclarations: Object}} options The options 534 | * @returns {Promise} The built visitor keys 535 | */ 536 | async function getKeysFromTsFile(file, options) { 537 | const code = await readFile(file); 538 | 539 | return getKeysFromTs(code, options); 540 | } 541 | 542 | //------------------------------------------------------------------------------ 543 | // Public Interface 544 | //------------------------------------------------------------------------------ 545 | 546 | export { alphabetizeKeyInterfaces, getKeysFromTs, getKeysFromTsFile }; 547 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2020"], 4 | "moduleResolution": "node", 5 | "module": "esnext", 6 | "resolveJsonModule": true, 7 | "allowJs": true, 8 | "checkJs": true, 9 | "noEmit": false, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "emitDeclarationOnly": true, 13 | "strict": true, 14 | "target": "es6", 15 | "outDir": "dist" 16 | }, 17 | "include": ["lib/**/*.js"], 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------