diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index a7e893e1..8b2be7f8 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -11,6 +11,8 @@ body: options: - label: "`espree`" required: false + - label: "`eslint-scope`" + required: false - type: textarea attributes: label: Environment diff --git a/.github/ISSUE_TEMPLATE/change.yml b/.github/ISSUE_TEMPLATE/change.yml index 75244e34..fb658fa9 100644 --- a/.github/ISSUE_TEMPLATE/change.yml +++ b/.github/ISSUE_TEMPLATE/change.yml @@ -10,6 +10,8 @@ body: options: - label: "`espree`" required: false + - label: "`eslint-scope`" + required: false - type: textarea attributes: label: What problem do you want to solve? diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml index 799a862d..a5f6954f 100644 --- a/.github/ISSUE_TEMPLATE/docs.yml +++ b/.github/ISSUE_TEMPLATE/docs.yml @@ -10,6 +10,8 @@ body: options: - label: "`espree`" required: false + - label: "`eslint-scope`" + required: false - type: textarea attributes: label: What documentation issue do you want to solve? diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 0f0a3494..518537bb 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -72,3 +72,29 @@ jobs: env: MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} MASTODON_HOST: ${{ secrets.MASTODON_HOST }} + + #----------------------------------------------------------------------------- + # eslint-scope + #----------------------------------------------------------------------------- + + - name: Publish eslint-scope package to npm + run: npm publish -w packages/eslint-scope --provenance + if: ${{ steps.release.outputs['packages/eslint-scope--release_created'] }} + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + - name: Tweet Release Announcement + run: npx @humanwhocodes/tweet "eslint-scope v${{ steps.release.outputs['packages/eslint-scope--major'] }}.${{ steps.release.outputs['packages/eslint-scope--minor'] }}.${{ steps.release.outputs['packages/eslint-scope--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/eslint-scope--tag_name'] }}" + if: ${{ steps.release.outputs['packages/eslint-scope--release_created'] }} + env: + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + + - name: Toot Release Announcement + run: npx @humanwhocodes/toot "eslint-scope v${{ steps.release.outputs['packages/eslint-scope--major'] }}.${{ steps.release.outputs['packages/eslint-scope--minor'] }}.${{ steps.release.outputs['packages/eslint-scope--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/eslint-scope--tag_name'] }}" + if: ${{ steps.release.outputs['packages/eslint-scope--release_created'] }} + env: + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} + MASTODON_HOST: ${{ secrets.MASTODON_HOST }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 18e97358..629fd483 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,4 @@ { - "packages/espree": "10.1.0" + "packages/espree": "10.1.0", + "packages/eslint-scope": "8.0.2" } diff --git a/README.md b/README.md index c8e99d1d..ba255454 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Monorepo for the JS language tools. This repository is the home of the following packages: - [`espree`](packages/espree) +- [`eslint-scope`](packages/eslint-scope) ## Security Policy diff --git a/eslint.config.js b/eslint.config.js index 4963ebbd..63018ca0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,6 @@ import eslintConfigESLint from "eslint-config-eslint"; import eslintConfigESLintFormatting from "eslint-config-eslint/formatting"; +import eslintPluginChaiFriendly from "eslint-plugin-chai-friendly"; import globals from "globals"; export default [ @@ -14,13 +15,46 @@ export default [ ...eslintConfigESLint, eslintConfigESLintFormatting, { - files: ["**/tests/lib/**"], + files: ["packages/espree/tests/lib/**"], languageOptions: { globals: { ...globals.mocha } } }, + { + files: ["packages/eslint-scope/tests/**"], + languageOptions: { + globals: { + ...globals.mocha + } + }, + plugins: { + "chai-friendly": eslintPluginChaiFriendly + }, + rules: { + "no-unused-expressions": "off", + "chai-friendly/no-unused-expressions": "error" + } + }, + { + files: ["packages/eslint-scope/lib/**"], + rules: { + "no-underscore-dangle": "off" + } + }, + { + files: ["packages/eslint-scope/Makefile.js"], + languageOptions: { + globals: { + ...globals.shelljs, + target: false + } + }, + rules: { + "no-console": "off" + } + }, { files: ["**/tools/**"], rules: { diff --git a/package.json b/package.json index 1bc4b25e..524d7c83 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "common-tags": "^1.8.2", "eslint": "^9.4.0", "eslint-config-eslint": "^11.0.0", + "eslint-plugin-chai-friendly": "^1.0.0", "globals": "^15.1.0", "got": "^14.4.1", "lint-staged": "^15.2.0", diff --git a/packages/eslint-scope/CHANGELOG.md b/packages/eslint-scope/CHANGELOG.md new file mode 100644 index 00000000..33e84b5b --- /dev/null +++ b/packages/eslint-scope/CHANGELOG.md @@ -0,0 +1,173 @@ +# Changelog + +## [8.0.2](https://github.com/eslint/eslint-scope/compare/v8.0.1...v8.0.2) (2024-07-08) + + +### Bug Fixes + +* `Definition#name` in catch patterns should be `Identifier` node ([#127](https://github.com/eslint/eslint-scope/issues/127)) ([8082caa](https://github.com/eslint/eslint-scope/commit/8082caa1a0f9aae0894a0a01fef9efa7a5e509f6)) + +## [8.0.1](https://github.com/eslint/eslint-scope/compare/v8.0.0...v8.0.1) (2024-03-20) + + +### Documentation + +* document Security Policy ([#122](https://github.com/eslint/eslint-scope/issues/122)) ([0d03700](https://github.com/eslint/eslint-scope/commit/0d0370035c3fcd3846bcfed25e2de1c90c204cc8)) + + +### Chores + +* switch to eslint flat config ([#121](https://github.com/eslint/eslint-scope/issues/121)) ([59b1606](https://github.com/eslint/eslint-scope/commit/59b160624bd725a1254024bcbbd28b7529c04c64)) +* upgrade espree@10.0.1, eslint-visitor-keys@4.0.0 ([#119](https://github.com/eslint/eslint-scope/issues/119)) ([6f95a94](https://github.com/eslint/eslint-scope/commit/6f95a9406e321ba519bbf635ebb8b1ae4b8861e7)) + +## [8.0.0](https://github.com/eslint/eslint-scope/compare/v7.2.2...v8.0.0) (2024-01-04) + + +### ⚠ BREAKING CHANGES + +* use ESTree `directive` property when searching for `"use strict"` ([#118](https://github.com/eslint/eslint-scope/issues/118)) +* class `extends` is evaluated in the class scope ([#116](https://github.com/eslint/eslint-scope/issues/116)) +* Require Node.js ^18.18.0 || ^20.9.0 || >=21.1.0 ([#115](https://github.com/eslint/eslint-scope/issues/115)) + +### Features + +* Require Node.js ^18.18.0 || ^20.9.0 || >=21.1.0 ([#115](https://github.com/eslint/eslint-scope/issues/115)) ([ed67857](https://github.com/eslint/eslint-scope/commit/ed678573aca7b00815ecb3c5dc4eee913b53a162)) +* use ESTree `directive` property when searching for `"use strict"` ([#118](https://github.com/eslint/eslint-scope/issues/118)) ([23fe81f](https://github.com/eslint/eslint-scope/commit/23fe81f5861ade17a2f17f9518fdde376dd2395f)) + + +### Bug Fixes + +* class `extends` is evaluated in the class scope ([#116](https://github.com/eslint/eslint-scope/issues/116)) ([42ef7a9](https://github.com/eslint/eslint-scope/commit/42ef7a995771f0700fc6af7eee03bab977f272c6)) + + +### Documentation + +* Update README with analyze() options ([#111](https://github.com/eslint/eslint-scope/issues/111)) ([2122fdb](https://github.com/eslint/eslint-scope/commit/2122fdb237cc0c115cd2473f383f741b1f055791)), closes [#110](https://github.com/eslint/eslint-scope/issues/110) + + +### Chores + +* Remove add-to-triage ([#106](https://github.com/eslint/eslint-scope/issues/106)) ([dc75851](https://github.com/eslint/eslint-scope/commit/dc75851b92b47eb37ed617448c0291129db676e1)) +* run tests in Node.js 21 ([#109](https://github.com/eslint/eslint-scope/issues/109)) ([957748e](https://github.com/eslint/eslint-scope/commit/957748e7fb741dd23f521af0c124ce6da0848997)) +* standardize npm script names ([#105](https://github.com/eslint/eslint-scope/issues/105)) ([115ded3](https://github.com/eslint/eslint-scope/commit/115ded3cb6f768a37f0dcb17bb16e2299849e16f)) + +## [7.2.2](https://github.com/eslint/eslint-scope/compare/v7.2.1...v7.2.2) (2023-07-27) + + +### Chores + +* Add PRs to triage ([#104](https://github.com/eslint/eslint-scope/issues/104)) ([a4dd888](https://github.com/eslint/eslint-scope/commit/a4dd8884726758ed513210a6b537105a07e8bf70)) +* generate provenance statements when release ([#102](https://github.com/eslint/eslint-scope/issues/102)) ([a27ce6b](https://github.com/eslint/eslint-scope/commit/a27ce6bbf70d7ba5af763a4d1650bfd87eee8136)) + +## [7.2.1](https://github.com/eslint/eslint-scope/compare/v7.2.0...v7.2.1) (2023-05-31) + + +### Chores + +* run tests on Node.js v20 ([#97](https://github.com/eslint/eslint-scope/issues/97)) ([675f7de](https://github.com/eslint/eslint-scope/commit/675f7de78c312546441fa9b204734c26376710f7)) +* set up release-please ([#99](https://github.com/eslint/eslint-scope/issues/99)) ([6bc2619](https://github.com/eslint/eslint-scope/commit/6bc2619fff2aa401fe43d3fda60e0c127d2d39a8)) + +v7.2.0 - April 13, 2023 + +* [`70c8db1`](https://github.com/eslint/eslint-scope/commit/70c8db16962830f20e27765cd4d1fd0e29b93c08) feat: Add isGlobalReturn method on scopeManager. (#96) (Nicholas C. Zakas) +* [`3dbad80`](https://github.com/eslint/eslint-scope/commit/3dbad80d98e5bb2453423dc3882500a7d76d6259) chore: add triage action (#95) (Milos Djermanovic) +* [`34ffedc`](https://github.com/eslint/eslint-scope/commit/34ffedc9645f3e5bf2111f766931efb0ff33040f) ci: add Node v19 (#94) (Milos Djermanovic) +* [`4c00534`](https://github.com/eslint/eslint-scope/commit/4c005347cd556b4fa97ba0b626decdd0fce95962) ci: update Github actions (#93) (Deepshika S) +* [`6c8ccf2`](https://github.com/eslint/eslint-scope/commit/6c8ccf223952daff78295907316d8d8c1e93cf89) chore: add funding field (#92) (Deepshika S) +* [`a8811b8`](https://github.com/eslint/eslint-scope/commit/a8811b89b93a8b6bb6ac7089d893d5686dabbeb8) build: add node v18 (#91) (唯然) +* [`25abacf`](https://github.com/eslint/eslint-scope/commit/25abacffe690b6141f19d59dc8c0e09599671508) docs: add badges (#89) (Milos Djermanovic) + +v7.1.1 - February 11, 2022 + +* [`8938109`](https://github.com/eslint/eslint-scope/commit/89381090cef60d8d47aeba111e04f859e063ae41) chore: upgrade espree@9.3.1 eslint-visitor-keys@3.3.0 (#88) (Milos Djermanovic) +* [`4e1d24c`](https://github.com/eslint/eslint-scope/commit/4e1d24ca4a747c14b37f059543cf08d1e1820b2d) fix: ignore `"use strict"` directives in ES3 (#87) (Milos Djermanovic) +* [`ceb8bdd`](https://github.com/eslint/eslint-scope/commit/ceb8bdd2cc31f67255e37a961096f9e3320abac6) ci: use node v16 (#84) (Nitin Kumar) +* [`62e147b`](https://github.com/eslint/eslint-scope/commit/62e147be60c1eb84a40c1918913755acbc2d3a3d) test: add tests with year-based `ecmaVersion` (#83) (Milos Djermanovic) + +v7.1.0 - November 20, 2021 + +* [`d756f1e`](https://github.com/eslint/eslint-scope/commit/d756f1ea974093c3ed7121d17f858254036b9690) feat: Add sourceType:commonjs support (#81) (Nicholas C. Zakas) + +v7.0.0 - November 16, 2021 + +* [`22a55c0`](https://github.com/eslint/eslint-scope/commit/22a55c01d1a28fd3ffd45c8818b49e65bd3e5005) feat!: support class static blocks (#80) (Milos Djermanovic) +* [`4aafb61`](https://github.com/eslint/eslint-scope/commit/4aafb616212adc39af37064932da912bdc7d9226) build: upgrade eslint-release to v3.2.0 to support conventional commits (#79) (Milos Djermanovic) +* [`263c762`](https://github.com/eslint/eslint-scope/commit/263c762432c5a3995e30fa814d02b0ed358b0e68) Build: add node v17 (#76) (唯然) + +v6.0.0 - July 23, 2021 + +* [`4ee1d80`](https://github.com/eslint/eslint-scope/commit/4ee1d80ce7dab961d9a158bc664d781bb663b570) Fix: Ensure correct version in package (#73) (Nicholas C. Zakas) +* [`82a7e6d`](https://github.com/eslint/eslint-scope/commit/82a7e6d9de8f4fca48e99779e9573dd46adbc18c) Breaking: Switch to ESM (fixes #70) (#71) (Brett Zamir) +* [`0b4a5f1`](https://github.com/eslint/eslint-scope/commit/0b4a5f132fb65520eee31bcd166078656b6e158e) Update: support class fields (refs eslint/eslint#14343) (#69) (Toru Nagashima) +* [`39f8cfc`](https://github.com/eslint/eslint-scope/commit/39f8cfc026d9b9b7c02e07368323350e74698f29) Chore: upgrade estraverse to version 5 (#68) (Rouven Weßling) +* [`ae27ff3`](https://github.com/eslint/eslint-scope/commit/ae27ff3692ab13cf62075b8659f0e17dfa44acd1) Docs: Add range to espree options in README (fixes #66) (#67) (Alan Liang) + +v5.1.1 - September 12, 2020 + +* [`9b528d7`](https://github.com/eslint/eslint-scope/commit/9b528d778c381718c12dabfb7f1c0e0dc6b36e49) Upgrade: esrecurse version to ^4.3.0 (#64) (Timofey Kachalov) +* [`f758bbc`](https://github.com/eslint/eslint-scope/commit/f758bbc3d49b9b9ea2289a5d6a6bba8dcf2c4903) Chore: fix definiton -> definition typo in comments (#63) (Kevin Kirsche) +* [`7513734`](https://github.com/eslint/eslint-scope/commit/751373473375b3f2edc4eaf1c8d2763d8435bb72) Chore: move to GitHub Actions (#62) (Kai Cataldo) + +v5.1.0 - June 4, 2020 + +* [`d4a3764`](https://github.com/eslint/eslint-scope/commit/d4a376434b16289c1a428d7e304576e997520873) Update: support new export syntax (#56) (Toru Nagashima) + +v5.0.0 - July 20, 2019 + +* [`e9fa22e`](https://github.com/eslint/eslint-scope/commit/e9fa22ea412c26cf2761fa98af7e715644bdb464) Upgrade: update dependencies after dropping support for Node <8 (#53) (Kai Cataldo) +* [`ee9f7c1`](https://github.com/eslint/eslint-scope/commit/ee9f7c12721aa195ba7e0e69551f49bfdb479951) Breaking: drop support for Node v6 (#54) (Kai Cataldo) + +v4.0.3 - March 15, 2019 + +* [`299df64`](https://github.com/eslint/eslint-scope/commit/299df64bdafb30b4d9372e4b7af0cf51a3818c4a) Fix: arrow function scope strictness (take 2) (#52) (futpib) + +v4.0.2 - March 1, 2019 + +* [`c925600`](https://github.com/eslint/eslint-scope/commit/c925600a684ae0f71b96f85339437a43b4d50d99) Revert "Fix: Arrow function scope strictness (fixes #49) (#50)" (#51) (Teddy Katz) + +v4.0.1 - March 1, 2019 + +* [`2533966`](https://github.com/eslint/eslint-scope/commit/2533966faf317df5a3847fab937ba462c16808b8) Fix: Arrow function scope strictness (fixes #49) (#50) (futpib) +* [`0cbeea5`](https://github.com/eslint/eslint-scope/commit/0cbeea51dfb66ab88ea34b0e3b4ad5e6cc210f2f) Chore: add supported Node.js versions to CI (#47) (Kai Cataldo) +* [`b423057`](https://github.com/eslint/eslint-scope/commit/b42305760638b8edf4667acf1445e450869bd983) Upgrade: eslint-release@1.0.0 (#46) (Teddy Katz) + +v4.0.0 - June 21, 2018 + + + +v4.0.0-rc.0 - June 9, 2018 + +* 3b919b8 Build: Adding rc release script to package.json (#38) (Kevin Partington) +* 137732a Chore: avoid creating package-lock.json files (#37) (Teddy Katz) + +v4.0.0-alpha.0 - April 27, 2018 + +* 7cc3769 Upgrade: eslint-release ^0.11.1 (#36) (Teddy Katz) +* c9f6967 Breaking: remove TDZScope (refs eslint/eslint#10245) (#35) (Toru Nagashima) +* 982a71f Fix: wrong resolution about default parameters (#33) (Toru Nagashima) +* 57889f1 Docs: Remove extra header line from LICENSE (#32) (Gyandeep Singh) + +v3.7.1 - April 12, 2017 + +* ced6262 Fix: restore previous Scope API exports from escope (#31) (Vitor Balocco) +* 5c3d966 Fix: Remove and Modify tests that contain invalid ES6 syntax (#29) (Reyad Attiyat) + +v3.7.0 - March 17, 2017 + +* 9e27835 Chore: Add files section to package.json (#24) (Ilya Volodin) +* 3e4d123 Upgrade: eslint-config-eslint to 4.0.0 (#21) (Teddy Katz) +* 38c50fb Chore: Rename src to lib and test to tests (#20) (Corbin Uselton) +* f4cd920 Chore: Remove esprima (#19) (Corbin Uselton) +* f81fad5 Revert "Chore: Remove esprima" (#18) (James Henry) +* 31b0085 Chore: Remove es6-map and es6-weakmap as they are included in node4 (#10) (#13) (Corbin Uselton) +* 12a1ca1 Add Makefile.js and eslint (#15) (Reyad Attiyat) +* 7d23f8e Chore: Remove es6-map and es6-weakmap as they are included in node4 (#10) (Corbin Uselton) +* 019441e Chore: Convert to ES6 that is supported on Node 4, commonjs modules and remove Babel (#14) (Corbin Uselton) +* c647f65 Update: Add check for node.body in referencer (#2) (Corbin Uselton) +* eb5c9db Remove browserify and jsdoc (#12) (Corbin Uselton) +* cf38df0 Chore: Update README.md (#3) (James Henry) +* 8a142ca Chore: Add eslint-release scripts (#6) (James Henry) +* e60d8cb Chore: Remove unused bower.json (#5) (James Henry) +* 049c545 Chore: Fix tests for eslint-scope (#4) (James Henry) +* f026aab Chore: Update package.json for eslint fork (#1) (James Henry) +* a94d281 Chore: Update license with JSF copyright (Nicholas C. Zakas) diff --git a/packages/eslint-scope/CONTRIBUTING.md b/packages/eslint-scope/CONTRIBUTING.md new file mode 100644 index 00000000..f1ddca9c --- /dev/null +++ b/packages/eslint-scope/CONTRIBUTING.md @@ -0,0 +1,5 @@ +## Project license: \ + +- You will only Submit Contributions where You have authored 100% of the content. +- You will only Submit Contributions to which You have the necessary rights. This means that if You are employed You have received the necessary permissions from Your employer to make the Contributions. +- Whatever content You Contribute will be provided under the Project License. diff --git a/packages/eslint-scope/LICENSE b/packages/eslint-scope/LICENSE new file mode 100644 index 00000000..d36a526f --- /dev/null +++ b/packages/eslint-scope/LICENSE @@ -0,0 +1,22 @@ +Copyright JS Foundation and other contributors, https://js.foundation +Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/eslint-scope/Makefile.js b/packages/eslint-scope/Makefile.js new file mode 100644 index 00000000..2859f328 --- /dev/null +++ b/packages/eslint-scope/Makefile.js @@ -0,0 +1,119 @@ +/** + * @fileoverview Build file + * @author nzakas + * @copyright OpenJS Foundation and other contributors, https://openjsf.org/ + * MIT License + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import "shelljs/make.js"; +import checker from "npm-license"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +// `shelljs/make.js` global command to unset any `set('-e')` (to exit upon +// first error) +set("+e"); + +//------------------------------------------------------------------------------ +// Settings +//------------------------------------------------------------------------------ + +const OPEN_SOURCE_LICENSES = [ + /MIT/u, /BSD/u, /Apache/u, /ISC/u, /WTF/u, /Public Domain/u +]; + +//------------------------------------------------------------------------------ +// Data +//------------------------------------------------------------------------------ + +const NODE = "node", + NODE_MODULES = "../../node_modules", + + // Utilities - intentional extra space at the end of each string + MOCHA = `${NODE_MODULES}/mocha/bin/_mocha `, + + // If switching back to Istanbul when may be working with ESM + // ISTANBUL = `${NODE} ${NODE_MODULES}/istanbul/lib/cli.js `, + C8 = `${NODE} ${NODE_MODULES}/c8/bin/c8.js`, + + // Files + TEST_FILES = "tests/*.js", + CJS_TEST_FILES = "tests/*.cjs"; + +//------------------------------------------------------------------------------ +// Tasks +//------------------------------------------------------------------------------ + +target.all = function() { + target.test(); +}; + +target.test = function() { + let errors = 0; + let lastReturn = exec(`${NODE} ${MOCHA} -- -R progress -c ${CJS_TEST_FILES}`); + + if (lastReturn.code !== 0) { + errors++; + } + + lastReturn = exec(`${C8} ${MOCHA} -- -R progress -c ${TEST_FILES}`); + + if (lastReturn.code !== 0) { + errors++; + } + + if (errors) { + exit(1); + } + + target.checkLicenses(); +}; + +target.checkLicenses = function() { + + /** + * Returns true if the given dependency's licenses are all permissable for use in OSS + * @param {Object} dependency object containing the name and licenses of the given dependency + * @returns {boolean} is permissable dependency + */ + function isPermissible(dependency) { + const licenses = dependency.licenses; + + if (Array.isArray(licenses)) { + return licenses.some(license => isPermissible({ + name: dependency.name, + licenses: license + })); + } + + return OPEN_SOURCE_LICENSES.some(license => license.test(licenses)); + } + + echo("Validating licenses"); + + checker.init({ + start: dirname, + meta: "./licenses-meta-data.json" + }, deps => { + const impermissible = Object.keys(deps).map(dependency => ({ + name: dependency, + licenses: deps[dependency].licenses + })).filter(dependency => !isPermissible(dependency)); + + if (impermissible.length) { + impermissible.forEach(dependency => { + console.error("%s license for %s is impermissible.", + dependency.licenses, + dependency.name); + }); + exit(1); + } + }); +}; diff --git a/packages/eslint-scope/README.md b/packages/eslint-scope/README.md new file mode 100644 index 00000000..68cdfb60 --- /dev/null +++ b/packages/eslint-scope/README.md @@ -0,0 +1,110 @@ +[![npm version](https://img.shields.io/npm/v/eslint-scope.svg)](https://www.npmjs.com/package/eslint-scope) +[![Downloads](https://img.shields.io/npm/dm/eslint-scope.svg)](https://www.npmjs.com/package/eslint-scope) +[![Build Status](https://github.com/eslint/eslint-scope/workflows/CI/badge.svg)](https://github.com/eslint/eslint-scope/actions) + +# ESLint Scope + +ESLint Scope is the [ECMAScript](http://www.ecma-international.org/publications/standards/Ecma-262.htm) scope analyzer used in ESLint. It is a fork of [escope](http://github.com/estools/escope). + +## Install + +``` +npm i eslint-scope --save +``` + +## 📖 Usage + +To use in an ESM file: + +```js +import * as eslintScope from 'eslint-scope'; +``` + +To use in a CommonJS file: + +```js +const eslintScope = require('eslint-scope'); +``` + +In order to analyze scope, you'll need to have an [ESTree](https://github.com/estree/estree) compliant AST structure to run it on. The primary method is `eslintScope.analyze()`, which takes two arguments: + +1. `ast` - the ESTree-compliant AST structure to analyze. +2. `options` (optional) - Options to adjust how the scope is analyzed, including: + * `ignoreEval` (default: `false`) - Set to `true` to ignore all `eval()` calls (which would normally create scopes). + * `nodejsScope` (default: `false`) - Set to `true` to create a top-level function scope needed for CommonJS evaluation. + * `impliedStrict` (default: `false`) - Set to `true` to evaluate the code in strict mode even outside of modules and without `"use strict"`. + * `ecmaVersion` (default: `5`) - The version of ECMAScript to use to evaluate the code. + * `sourceType` (default: `"script"`) - The type of JavaScript file to evaluate. Change to `"module"` for ECMAScript module code. + * `childVisitorKeys` (default: `null`) - An object with visitor key information (like [`eslint-visitor-keys`](https://github.com/eslint/eslint-visitor-keys)). Without this, `eslint-scope` finds child nodes to visit algorithmically. Providing this option is a performance enhancement. + * `fallback` (default: `"iteration"`) - The strategy to use when `childVisitorKeys` is not specified. May be a function. + +Example: + +```js +import * as eslintScope from 'eslint-scope'; +import * as espree from 'espree'; +import estraverse from 'estraverse'; + +const options = { + ecmaVersion: 2022, + sourceType: "module" +}; + +const ast = espree.parse(code, { range: true, ...options }); +const scopeManager = eslintScope.analyze(ast, options); + +const currentScope = scopeManager.acquire(ast); // global scope + +estraverse.traverse(ast, { + enter (node, parent) { + // do stuff + + if (/Function/.test(node.type)) { + currentScope = scopeManager.acquire(node); // get current function scope + } + }, + leave(node, parent) { + if (/Function/.test(node.type)) { + currentScope = currentScope.upper; // set to parent scope + } + + // do stuff + } +}); +``` + +## Contributing + +Issues and pull requests will be triaged and responded to as quickly as possible. We operate under the [ESLint Contributor Guidelines](http://eslint.org/docs/developer-guide/contributing), so please be sure to read them before contributing. If you're not sure where to dig in, check out the [issues](https://github.com/eslint/eslint-scope/issues). + +## Security Policy + +We work hard to ensure that ESLint Scope is safe for everyone and that security issues are addressed quickly and responsibly. Read the full [security policy](https://github.com/eslint/.github/blob/master/SECURITY.md). + +## Build Commands + +* `npm test` - run all linting and tests +* `npm run lint` - run all linting + +## License + +ESLint Scope is licensed under a permissive BSD 2-clause license. + +## Sponsors + +The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate) to get your logo on our README and website. + + + +

Platinum Sponsors

+

Automattic Airbnb

Gold Sponsors

+

Eli Schleifer Salesforce

Silver Sponsors

+

JetBrains Liftoff American Express Workleap

Bronze Sponsors

+

notion Anagram Solver Icons8 Discord Ignition Nx HeroCoders Nextbase Starter Kit

+ + + +

Technology Sponsors

+

Netlify Algolia 1Password +

+ diff --git a/packages/eslint-scope/lib/definition.js b/packages/eslint-scope/lib/definition.js new file mode 100644 index 00000000..9744ef48 --- /dev/null +++ b/packages/eslint-scope/lib/definition.js @@ -0,0 +1,85 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import Variable from "./variable.js"; + +/** + * @constructor Definition + */ +class Definition { + constructor(type, name, node, parent, index, kind) { + + /** + * @member {string} Definition#type - type of the occurrence (e.g. "Parameter", "Variable", ...). + */ + this.type = type; + + /** + * @member {espree.Identifier} Definition#name - the identifier AST node of the occurrence. + */ + this.name = name; + + /** + * @member {espree.Node} Definition#node - the enclosing node of the identifier. + */ + this.node = node; + + /** + * @member {espree.Node?} Definition#parent - the enclosing statement node of the identifier. + */ + this.parent = parent; + + /** + * @member {number?} Definition#index - the index in the declaration statement. + */ + this.index = index; + + /** + * @member {string?} Definition#kind - the kind of the declaration statement. + */ + this.kind = kind; + } +} + +/** + * @constructor ParameterDefinition + */ +class ParameterDefinition extends Definition { + constructor(name, node, index, rest) { + super(Variable.Parameter, name, node, null, index, null); + + /** + * Whether the parameter definition is a part of a rest parameter. + * @member {boolean} ParameterDefinition#rest + */ + this.rest = rest; + } +} + +export { + ParameterDefinition, + Definition +}; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/index.js b/packages/eslint-scope/lib/index.js new file mode 100644 index 00000000..5a27d062 --- /dev/null +++ b/packages/eslint-scope/lib/index.js @@ -0,0 +1,169 @@ +/* + Copyright (C) 2012-2014 Yusuke Suzuki + Copyright (C) 2013 Alex Seville + Copyright (C) 2014 Thiago de Arruda + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * Escope (escope) is an ECMAScript + * scope analyzer extracted from the esmangle project. + *

+ * escope finds lexical scopes in a source program, i.e. areas of that + * program where different occurrences of the same identifier refer to the same + * variable. With each scope the contained variables are collected, and each + * identifier reference in code is linked to its corresponding variable (if + * possible). + *

+ * escope works on a syntax tree of the parsed source code which has + * to adhere to the + * Mozilla Parser API. E.g. espree is a parser + * that produces such syntax trees. + *

+ * The main interface is the {@link analyze} function. + * @module escope + */ + +import assert from "node:assert"; + +import ScopeManager from "./scope-manager.js"; +import Referencer from "./referencer.js"; +import Reference from "./reference.js"; +import Variable from "./variable.js"; + +import eslintScopeVersion from "./version.js"; + +/** + * Set the default options + * @returns {Object} options + */ +function defaultOptions() { + return { + optimistic: false, + nodejsScope: false, + impliedStrict: false, + sourceType: "script", // one of ['script', 'module', 'commonjs'] + ecmaVersion: 5, + childVisitorKeys: null, + fallback: "iteration" + }; +} + +/** + * Preform deep update on option object + * @param {Object} target Options + * @param {Object} override Updates + * @returns {Object} Updated options + */ +function updateDeeply(target, override) { + + /** + * Is hash object + * @param {Object} value Test value + * @returns {boolean} Result + */ + function isHashObject(value) { + return typeof value === "object" && value instanceof Object && !(value instanceof Array) && !(value instanceof RegExp); + } + + for (const key in override) { + if (Object.hasOwn(override, key)) { + const val = override[key]; + + if (isHashObject(val)) { + if (isHashObject(target[key])) { + updateDeeply(target[key], val); + } else { + target[key] = updateDeeply({}, val); + } + } else { + target[key] = val; + } + } + } + return target; +} + +/** + * Main interface function. Takes an Espree syntax tree and returns the + * analyzed scopes. + * @function analyze + * @param {espree.Tree} tree Abstract Syntax Tree + * @param {Object} providedOptions Options that tailor the scope analysis + * @param {boolean} [providedOptions.optimistic=false] the optimistic flag + * @param {boolean} [providedOptions.ignoreEval=false] whether to check 'eval()' calls + * @param {boolean} [providedOptions.nodejsScope=false] whether the whole + * script is executed under node.js environment. When enabled, escope adds + * a function scope immediately following the global scope. + * @param {boolean} [providedOptions.impliedStrict=false] implied strict mode + * (if ecmaVersion >= 5). + * @param {string} [providedOptions.sourceType='script'] the source type of the script. one of 'script', 'module', and 'commonjs' + * @param {number} [providedOptions.ecmaVersion=5] which ECMAScript version is considered + * @param {Object} [providedOptions.childVisitorKeys=null] Additional known visitor keys. See [esrecurse](https://github.com/estools/esrecurse)'s the `childVisitorKeys` option. + * @param {string} [providedOptions.fallback='iteration'] A kind of the fallback in order to encounter with unknown node. See [esrecurse](https://github.com/estools/esrecurse)'s the `fallback` option. + * @returns {ScopeManager} ScopeManager + */ +function analyze(tree, providedOptions) { + const options = updateDeeply(defaultOptions(), providedOptions); + const scopeManager = new ScopeManager(options); + const referencer = new Referencer(options, scopeManager); + + referencer.visit(tree); + + assert(scopeManager.__currentScope === null, "currentScope should be null."); + + return scopeManager; +} + +export { + + /** @name module:escope.version */ + eslintScopeVersion as version, + + /** @name module:escope.Reference */ + Reference, + + /** @name module:escope.Variable */ + Variable, + + /** @name module:escope.ScopeManager */ + ScopeManager, + + /** @name module:escope.Referencer */ + Referencer, + + analyze +}; + +/** @name module:escope.Definition */ +export { Definition } from "./definition.js"; + +/** @name module:escope.PatternVisitor */ +export { default as PatternVisitor } from "./pattern-visitor.js"; + +/** @name module:escope.Scope */ +export { Scope } from "./scope.js"; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/pattern-visitor.js b/packages/eslint-scope/lib/pattern-visitor.js new file mode 100644 index 00000000..367a3773 --- /dev/null +++ b/packages/eslint-scope/lib/pattern-visitor.js @@ -0,0 +1,154 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import estraverse from "estraverse"; +import esrecurse from "esrecurse"; + +const { Syntax } = estraverse; + +/** + * Get last array element + * @param {Array} xs array + * @returns {any} Last elment + */ +function getLast(xs) { + return xs.at(-1) || null; +} + +/** + * Visitor for destructuring patterns. + */ +class PatternVisitor extends esrecurse.Visitor { + static isPattern(node) { + const nodeType = node.type; + + return ( + nodeType === Syntax.Identifier || + nodeType === Syntax.ObjectPattern || + nodeType === Syntax.ArrayPattern || + nodeType === Syntax.SpreadElement || + nodeType === Syntax.RestElement || + nodeType === Syntax.AssignmentPattern + ); + } + + constructor(options, rootPattern, callback) { + super(null, options); + this.rootPattern = rootPattern; + this.callback = callback; + this.assignments = []; + this.rightHandNodes = []; + this.restElements = []; + } + + Identifier(pattern) { + const lastRestElement = getLast(this.restElements); + + this.callback(pattern, { + topLevel: pattern === this.rootPattern, + rest: lastRestElement !== null && lastRestElement !== void 0 && lastRestElement.argument === pattern, + assignments: this.assignments + }); + } + + Property(property) { + + // Computed property's key is a right hand node. + if (property.computed) { + this.rightHandNodes.push(property.key); + } + + // If it's shorthand, its key is same as its value. + // If it's shorthand and has its default value, its key is same as its value.left (the value is AssignmentPattern). + // If it's not shorthand, the name of new variable is its value's. + this.visit(property.value); + } + + ArrayPattern(pattern) { + for (let i = 0, iz = pattern.elements.length; i < iz; ++i) { + const element = pattern.elements[i]; + + this.visit(element); + } + } + + AssignmentPattern(pattern) { + this.assignments.push(pattern); + this.visit(pattern.left); + this.rightHandNodes.push(pattern.right); + this.assignments.pop(); + } + + RestElement(pattern) { + this.restElements.push(pattern); + this.visit(pattern.argument); + this.restElements.pop(); + } + + MemberExpression(node) { + + // Computed property's key is a right hand node. + if (node.computed) { + this.rightHandNodes.push(node.property); + } + + // the object is only read, write to its property. + this.rightHandNodes.push(node.object); + } + + // + // ForInStatement.left and AssignmentExpression.left are LeftHandSideExpression. + // By spec, LeftHandSideExpression is Pattern or MemberExpression. + // (see also: https://github.com/estree/estree/pull/20#issuecomment-74584758) + // But espree 2.0 parses to ArrayExpression, ObjectExpression, etc... + // + + SpreadElement(node) { + this.visit(node.argument); + } + + ArrayExpression(node) { + node.elements.forEach(this.visit, this); + } + + AssignmentExpression(node) { + this.assignments.push(node); + this.visit(node.left); + this.rightHandNodes.push(node.right); + this.assignments.pop(); + } + + CallExpression(node) { + + // arguments are right hand nodes. + node.arguments.forEach(a => { + this.rightHandNodes.push(a); + }); + this.visit(node.callee); + } +} + +export default PatternVisitor; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/reference.js b/packages/eslint-scope/lib/reference.js new file mode 100644 index 00000000..e657d628 --- /dev/null +++ b/packages/eslint-scope/lib/reference.js @@ -0,0 +1,166 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const READ = 0x1; +const WRITE = 0x2; +const RW = READ | WRITE; + +/** + * A Reference represents a single occurrence of an identifier in code. + * @constructor Reference + */ +class Reference { + constructor(ident, scope, flag, writeExpr, maybeImplicitGlobal, partial, init) { + + /** + * Identifier syntax node. + * @member {espreeIdentifier} Reference#identifier + */ + this.identifier = ident; + + /** + * Reference to the enclosing Scope. + * @member {Scope} Reference#from + */ + this.from = scope; + + /** + * Whether the reference comes from a dynamic scope (such as 'eval', + * 'with', etc.), and may be trapped by dynamic scopes. + * @member {boolean} Reference#tainted + */ + this.tainted = false; + + /** + * The variable this reference is resolved with. + * @member {Variable} Reference#resolved + */ + this.resolved = null; + + /** + * The read-write mode of the reference. (Value is one of {@link + * Reference.READ}, {@link Reference.RW}, {@link Reference.WRITE}). + * @member {number} Reference#flag + * @private + */ + this.flag = flag; + if (this.isWrite()) { + + /** + * If reference is writeable, this is the tree being written to it. + * @member {espreeNode} Reference#writeExpr + */ + this.writeExpr = writeExpr; + + /** + * Whether the Reference might refer to a partial value of writeExpr. + * @member {boolean} Reference#partial + */ + this.partial = partial; + + /** + * Whether the Reference is to write of initialization. + * @member {boolean} Reference#init + */ + this.init = init; + } + this.__maybeImplicitGlobal = maybeImplicitGlobal; + } + + /** + * Whether the reference is static. + * @function Reference#isStatic + * @returns {boolean} static + */ + isStatic() { + return !this.tainted && this.resolved && this.resolved.scope.isStatic(); + } + + /** + * Whether the reference is writeable. + * @function Reference#isWrite + * @returns {boolean} write + */ + isWrite() { + return !!(this.flag & Reference.WRITE); + } + + /** + * Whether the reference is readable. + * @function Reference#isRead + * @returns {boolean} read + */ + isRead() { + return !!(this.flag & Reference.READ); + } + + /** + * Whether the reference is read-only. + * @function Reference#isReadOnly + * @returns {boolean} read only + */ + isReadOnly() { + return this.flag === Reference.READ; + } + + /** + * Whether the reference is write-only. + * @function Reference#isWriteOnly + * @returns {boolean} write only + */ + isWriteOnly() { + return this.flag === Reference.WRITE; + } + + /** + * Whether the reference is read-write. + * @function Reference#isReadWrite + * @returns {boolean} read write + */ + isReadWrite() { + return this.flag === Reference.RW; + } +} + +/** + * @constant Reference.READ + * @private + */ +Reference.READ = READ; + +/** + * @constant Reference.WRITE + * @private + */ +Reference.WRITE = WRITE; + +/** + * @constant Reference.RW + * @private + */ +Reference.RW = RW; + +export default Reference; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/referencer.js b/packages/eslint-scope/lib/referencer.js new file mode 100644 index 00000000..cf6ea999 --- /dev/null +++ b/packages/eslint-scope/lib/referencer.js @@ -0,0 +1,656 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import estraverse from "estraverse"; +import esrecurse from "esrecurse"; +import Reference from "./reference.js"; +import Variable from "./variable.js"; +import PatternVisitor from "./pattern-visitor.js"; +import { Definition, ParameterDefinition } from "./definition.js"; +import assert from "node:assert"; + +const { Syntax } = estraverse; + +/** + * Traverse identifier in pattern + * @param {Object} options options + * @param {pattern} rootPattern root pattern + * @param {Refencer} referencer referencer + * @param {callback} callback callback + * @returns {void} + */ +function traverseIdentifierInPattern(options, rootPattern, referencer, callback) { + + // Call the callback at left hand identifier nodes, and Collect right hand nodes. + const visitor = new PatternVisitor(options, rootPattern, callback); + + visitor.visit(rootPattern); + + // Process the right hand nodes recursively. + if (referencer !== null && referencer !== void 0) { + visitor.rightHandNodes.forEach(referencer.visit, referencer); + } +} + +// Importing ImportDeclaration. +// http://people.mozilla.org/~jorendorff/es6-draft.html#sec-moduledeclarationinstantiation +// https://github.com/estree/estree/blob/master/es6.md#importdeclaration +// FIXME: Now, we don't create module environment, because the context is +// implementation dependent. + +/** + * Visitor for import specifiers. + */ +class Importer extends esrecurse.Visitor { + constructor(declaration, referencer) { + super(null, referencer.options); + this.declaration = declaration; + this.referencer = referencer; + } + + visitImport(id, specifier) { + this.referencer.visitPattern(id, pattern => { + this.referencer.currentScope().__define(pattern, + new Definition( + Variable.ImportBinding, + pattern, + specifier, + this.declaration, + null, + null + )); + }); + } + + ImportNamespaceSpecifier(node) { + const local = (node.local || node.id); + + if (local) { + this.visitImport(local, node); + } + } + + ImportDefaultSpecifier(node) { + const local = (node.local || node.id); + + this.visitImport(local, node); + } + + ImportSpecifier(node) { + const local = (node.local || node.id); + + if (node.name) { + this.visitImport(node.name, node); + } else { + this.visitImport(local, node); + } + } +} + +/** + * Referencing variables and creating bindings. + */ +class Referencer extends esrecurse.Visitor { + constructor(options, scopeManager) { + super(null, options); + this.options = options; + this.scopeManager = scopeManager; + this.parent = null; + this.isInnerMethodDefinition = false; + } + + currentScope() { + return this.scopeManager.__currentScope; + } + + close(node) { + while (this.currentScope() && node === this.currentScope().block) { + this.scopeManager.__currentScope = this.currentScope().__close(this.scopeManager); + } + } + + pushInnerMethodDefinition(isInnerMethodDefinition) { + const previous = this.isInnerMethodDefinition; + + this.isInnerMethodDefinition = isInnerMethodDefinition; + return previous; + } + + popInnerMethodDefinition(isInnerMethodDefinition) { + this.isInnerMethodDefinition = isInnerMethodDefinition; + } + + referencingDefaultValue(pattern, assignments, maybeImplicitGlobal, init) { + const scope = this.currentScope(); + + assignments.forEach(assignment => { + scope.__referencing( + pattern, + Reference.WRITE, + assignment.right, + maybeImplicitGlobal, + pattern !== assignment.left, + init + ); + }); + } + + visitPattern(node, options, callback) { + let visitPatternOptions = options; + let visitPatternCallback = callback; + + if (typeof options === "function") { + visitPatternCallback = options; + visitPatternOptions = { processRightHandNodes: false }; + } + + traverseIdentifierInPattern( + this.options, + node, + visitPatternOptions.processRightHandNodes ? this : null, + visitPatternCallback + ); + } + + visitFunction(node) { + let i, iz; + + // FunctionDeclaration name is defined in upper scope + // NOTE: Not referring variableScope. It is intended. + // Since + // in ES5, FunctionDeclaration should be in FunctionBody. + // in ES6, FunctionDeclaration should be block scoped. + + if (node.type === Syntax.FunctionDeclaration) { + + // id is defined in upper scope + this.currentScope().__define(node.id, + new Definition( + Variable.FunctionName, + node.id, + node, + null, + null, + null + )); + } + + // FunctionExpression with name creates its special scope; + // FunctionExpressionNameScope. + if (node.type === Syntax.FunctionExpression && node.id) { + this.scopeManager.__nestFunctionExpressionNameScope(node); + } + + // Consider this function is in the MethodDefinition. + this.scopeManager.__nestFunctionScope(node, this.isInnerMethodDefinition); + + const that = this; + + /** + * Visit pattern callback + * @param {pattern} pattern pattern + * @param {Object} info info + * @returns {void} + */ + function visitPatternCallback(pattern, info) { + that.currentScope().__define(pattern, + new ParameterDefinition( + pattern, + node, + i, + info.rest + )); + + that.referencingDefaultValue(pattern, info.assignments, null, true); + } + + // Process parameter declarations. + for (i = 0, iz = node.params.length; i < iz; ++i) { + this.visitPattern(node.params[i], { processRightHandNodes: true }, visitPatternCallback); + } + + // if there's a rest argument, add that + if (node.rest) { + this.visitPattern({ + type: "RestElement", + argument: node.rest + }, pattern => { + this.currentScope().__define(pattern, + new ParameterDefinition( + pattern, + node, + node.params.length, + true + )); + }); + } + + // In TypeScript there are a number of function-like constructs which have no body, + // so check it exists before traversing + if (node.body) { + + // Skip BlockStatement to prevent creating BlockStatement scope. + if (node.body.type === Syntax.BlockStatement) { + this.visitChildren(node.body); + } else { + this.visit(node.body); + } + } + + this.close(node); + } + + visitClass(node) { + if (node.type === Syntax.ClassDeclaration) { + this.currentScope().__define(node.id, + new Definition( + Variable.ClassName, + node.id, + node, + null, + null, + null + )); + } + + this.scopeManager.__nestClassScope(node); + + if (node.id) { + this.currentScope().__define(node.id, + new Definition( + Variable.ClassName, + node.id, + node + )); + } + + this.visit(node.superClass); + this.visit(node.body); + + this.close(node); + } + + visitProperty(node) { + let previous; + + if (node.computed) { + this.visit(node.key); + } + + const isMethodDefinition = node.type === Syntax.MethodDefinition; + + if (isMethodDefinition) { + previous = this.pushInnerMethodDefinition(true); + } + this.visit(node.value); + if (isMethodDefinition) { + this.popInnerMethodDefinition(previous); + } + } + + visitForIn(node) { + if (node.left.type === Syntax.VariableDeclaration && node.left.kind !== "var") { + this.scopeManager.__nestForScope(node); + } + + if (node.left.type === Syntax.VariableDeclaration) { + this.visit(node.left); + this.visitPattern(node.left.declarations[0].id, pattern => { + this.currentScope().__referencing(pattern, Reference.WRITE, node.right, null, true, true); + }); + } else { + this.visitPattern(node.left, { processRightHandNodes: true }, (pattern, info) => { + let maybeImplicitGlobal = null; + + if (!this.currentScope().isStrict) { + maybeImplicitGlobal = { + pattern, + node + }; + } + this.referencingDefaultValue(pattern, info.assignments, maybeImplicitGlobal, false); + this.currentScope().__referencing(pattern, Reference.WRITE, node.right, maybeImplicitGlobal, true, false); + }); + } + this.visit(node.right); + this.visit(node.body); + + this.close(node); + } + + visitVariableDeclaration(variableTargetScope, type, node, index) { + + const decl = node.declarations[index]; + const init = decl.init; + + this.visitPattern(decl.id, { processRightHandNodes: true }, (pattern, info) => { + variableTargetScope.__define( + pattern, + new Definition( + type, + pattern, + decl, + node, + index, + node.kind + ) + ); + + this.referencingDefaultValue(pattern, info.assignments, null, true); + if (init) { + this.currentScope().__referencing(pattern, Reference.WRITE, init, null, !info.topLevel, true); + } + }); + } + + AssignmentExpression(node) { + if (PatternVisitor.isPattern(node.left)) { + if (node.operator === "=") { + this.visitPattern(node.left, { processRightHandNodes: true }, (pattern, info) => { + let maybeImplicitGlobal = null; + + if (!this.currentScope().isStrict) { + maybeImplicitGlobal = { + pattern, + node + }; + } + this.referencingDefaultValue(pattern, info.assignments, maybeImplicitGlobal, false); + this.currentScope().__referencing(pattern, Reference.WRITE, node.right, maybeImplicitGlobal, !info.topLevel, false); + }); + } else { + this.currentScope().__referencing(node.left, Reference.RW, node.right); + } + } else { + this.visit(node.left); + } + this.visit(node.right); + } + + CatchClause(node) { + this.scopeManager.__nestCatchScope(node); + + this.visitPattern(node.param, { processRightHandNodes: true }, (pattern, info) => { + this.currentScope().__define(pattern, + new Definition( + Variable.CatchClause, + pattern, + node, + null, + null, + null + )); + this.referencingDefaultValue(pattern, info.assignments, null, true); + }); + this.visit(node.body); + + this.close(node); + } + + Program(node) { + this.scopeManager.__nestGlobalScope(node); + + if (this.scopeManager.isGlobalReturn()) { + + // Force strictness of GlobalScope to false when using node.js scope. + this.currentScope().isStrict = false; + this.scopeManager.__nestFunctionScope(node, false); + } + + if (this.scopeManager.__isES6() && this.scopeManager.isModule()) { + this.scopeManager.__nestModuleScope(node); + } + + if (this.scopeManager.isStrictModeSupported() && this.scopeManager.isImpliedStrict()) { + this.currentScope().isStrict = true; + } + + this.visitChildren(node); + this.close(node); + } + + Identifier(node) { + this.currentScope().__referencing(node); + } + + // eslint-disable-next-line class-methods-use-this -- Desired as instance method + PrivateIdentifier() { + + // Do nothing. + } + + UpdateExpression(node) { + if (PatternVisitor.isPattern(node.argument)) { + this.currentScope().__referencing(node.argument, Reference.RW, null); + } else { + this.visitChildren(node); + } + } + + MemberExpression(node) { + this.visit(node.object); + if (node.computed) { + this.visit(node.property); + } + } + + Property(node) { + this.visitProperty(node); + } + + PropertyDefinition(node) { + const { computed, key, value } = node; + + if (computed) { + this.visit(key); + } + if (value) { + this.scopeManager.__nestClassFieldInitializerScope(value); + this.visit(value); + this.close(value); + } + } + + StaticBlock(node) { + this.scopeManager.__nestClassStaticBlockScope(node); + + this.visitChildren(node); + + this.close(node); + } + + MethodDefinition(node) { + this.visitProperty(node); + } + + BreakStatement() {} // eslint-disable-line class-methods-use-this -- Desired as instance method + + ContinueStatement() {} // eslint-disable-line class-methods-use-this -- Desired as instance method + + LabeledStatement(node) { + this.visit(node.body); + } + + ForStatement(node) { + + // Create ForStatement declaration. + // NOTE: In ES6, ForStatement dynamically generates + // per iteration environment. However, escope is + // a static analyzer, we only generate one scope for ForStatement. + if (node.init && node.init.type === Syntax.VariableDeclaration && node.init.kind !== "var") { + this.scopeManager.__nestForScope(node); + } + + this.visitChildren(node); + + this.close(node); + } + + ClassExpression(node) { + this.visitClass(node); + } + + ClassDeclaration(node) { + this.visitClass(node); + } + + CallExpression(node) { + + // Check this is direct call to eval + if (!this.scopeManager.__ignoreEval() && node.callee.type === Syntax.Identifier && node.callee.name === "eval") { + + // NOTE: This should be `variableScope`. Since direct eval call always creates Lexical environment and + // let / const should be enclosed into it. Only VariableDeclaration affects on the caller's environment. + this.currentScope().variableScope.__detectEval(); + } + this.visitChildren(node); + } + + BlockStatement(node) { + if (this.scopeManager.__isES6()) { + this.scopeManager.__nestBlockScope(node); + } + + this.visitChildren(node); + + this.close(node); + } + + ThisExpression() { + this.currentScope().variableScope.__detectThis(); + } + + WithStatement(node) { + this.visit(node.object); + + // Then nest scope for WithStatement. + this.scopeManager.__nestWithScope(node); + + this.visit(node.body); + + this.close(node); + } + + VariableDeclaration(node) { + const variableTargetScope = (node.kind === "var") ? this.currentScope().variableScope : this.currentScope(); + + for (let i = 0, iz = node.declarations.length; i < iz; ++i) { + const decl = node.declarations[i]; + + this.visitVariableDeclaration(variableTargetScope, Variable.Variable, node, i); + if (decl.init) { + this.visit(decl.init); + } + } + } + + // sec 13.11.8 + SwitchStatement(node) { + this.visit(node.discriminant); + + if (this.scopeManager.__isES6()) { + this.scopeManager.__nestSwitchScope(node); + } + + for (let i = 0, iz = node.cases.length; i < iz; ++i) { + this.visit(node.cases[i]); + } + + this.close(node); + } + + FunctionDeclaration(node) { + this.visitFunction(node); + } + + FunctionExpression(node) { + this.visitFunction(node); + } + + ForOfStatement(node) { + this.visitForIn(node); + } + + ForInStatement(node) { + this.visitForIn(node); + } + + ArrowFunctionExpression(node) { + this.visitFunction(node); + } + + ImportDeclaration(node) { + assert(this.scopeManager.__isES6() && this.scopeManager.isModule(), "ImportDeclaration should appear when the mode is ES6 and in the module context."); + + const importer = new Importer(node, this); + + importer.visit(node); + } + + visitExportDeclaration(node) { + if (node.source) { + return; + } + if (node.declaration) { + this.visit(node.declaration); + return; + } + + this.visitChildren(node); + } + + // TODO: ExportDeclaration doesn't exist. for bc? + ExportDeclaration(node) { + this.visitExportDeclaration(node); + } + + ExportAllDeclaration(node) { + this.visitExportDeclaration(node); + } + + ExportDefaultDeclaration(node) { + this.visitExportDeclaration(node); + } + + ExportNamedDeclaration(node) { + this.visitExportDeclaration(node); + } + + ExportSpecifier(node) { + + // TODO: `node.id` doesn't exist. for bc? + const local = (node.id || node.local); + + this.visit(local); + } + + MetaProperty() { // eslint-disable-line class-methods-use-this -- Desired as instance method + + // do nothing. + } +} + +export default Referencer; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/scope-manager.js b/packages/eslint-scope/lib/scope-manager.js new file mode 100644 index 00000000..077b0491 --- /dev/null +++ b/packages/eslint-scope/lib/scope-manager.js @@ -0,0 +1,249 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import { + BlockScope, + CatchScope, + ClassFieldInitializerScope, + ClassStaticBlockScope, + ClassScope, + ForScope, + FunctionExpressionNameScope, + FunctionScope, + GlobalScope, + ModuleScope, + SwitchScope, + WithScope +} from "./scope.js"; +import assert from "node:assert"; + +/** + * @constructor ScopeManager + */ +class ScopeManager { + constructor(options) { + this.scopes = []; + this.globalScope = null; + this.__nodeToScope = new WeakMap(); + this.__currentScope = null; + this.__options = options; + this.__declaredVariables = new WeakMap(); + } + + __isOptimistic() { + return this.__options.optimistic; + } + + __ignoreEval() { + return this.__options.ignoreEval; + } + + isGlobalReturn() { + return this.__options.nodejsScope || this.__options.sourceType === "commonjs"; + } + + isModule() { + return this.__options.sourceType === "module"; + } + + isImpliedStrict() { + return this.__options.impliedStrict; + } + + isStrictModeSupported() { + return this.__options.ecmaVersion >= 5; + } + + // Returns appropriate scope for this node. + __get(node) { + return this.__nodeToScope.get(node); + } + + /** + * Get variables that are declared by the node. + * + * "are declared by the node" means the node is same as `Variable.defs[].node` or `Variable.defs[].parent`. + * If the node declares nothing, this method returns an empty array. + * CAUTION: This API is experimental. See https://github.com/estools/escope/pull/69 for more details. + * @param {Espree.Node} node a node to get. + * @returns {Variable[]} variables that declared by the node. + */ + getDeclaredVariables(node) { + return this.__declaredVariables.get(node) || []; + } + + /** + * acquire scope from node. + * @function ScopeManager#acquire + * @param {Espree.Node} node node for the acquired scope. + * @param {?boolean} [inner=false] look up the most inner scope, default value is false. + * @returns {Scope?} Scope from node + */ + acquire(node, inner) { + + /** + * predicate + * @param {Scope} testScope scope to test + * @returns {boolean} predicate + */ + function predicate(testScope) { + if (testScope.type === "function" && testScope.functionExpressionScope) { + return false; + } + return true; + } + + const scopes = this.__get(node); + + if (!scopes || scopes.length === 0) { + return null; + } + + // Heuristic selection from all scopes. + // If you would like to get all scopes, please use ScopeManager#acquireAll. + if (scopes.length === 1) { + return scopes[0]; + } + + if (inner) { + for (let i = scopes.length - 1; i >= 0; --i) { + const scope = scopes[i]; + + if (predicate(scope)) { + return scope; + } + } + } else { + for (let i = 0, iz = scopes.length; i < iz; ++i) { + const scope = scopes[i]; + + if (predicate(scope)) { + return scope; + } + } + } + + return null; + } + + /** + * acquire all scopes from node. + * @function ScopeManager#acquireAll + * @param {Espree.Node} node node for the acquired scope. + * @returns {Scopes?} Scope array + */ + acquireAll(node) { + return this.__get(node); + } + + /** + * release the node. + * @function ScopeManager#release + * @param {Espree.Node} node releasing node. + * @param {?boolean} [inner=false] look up the most inner scope, default value is false. + * @returns {Scope?} upper scope for the node. + */ + release(node, inner) { + const scopes = this.__get(node); + + if (scopes && scopes.length) { + const scope = scopes[0].upper; + + if (!scope) { + return null; + } + return this.acquire(scope.block, inner); + } + return null; + } + + attach() { } // eslint-disable-line class-methods-use-this -- Desired as instance method + + detach() { } // eslint-disable-line class-methods-use-this -- Desired as instance method + + __nestScope(scope) { + if (scope instanceof GlobalScope) { + assert(this.__currentScope === null); + this.globalScope = scope; + } + this.__currentScope = scope; + return scope; + } + + __nestGlobalScope(node) { + return this.__nestScope(new GlobalScope(this, node)); + } + + __nestBlockScope(node) { + return this.__nestScope(new BlockScope(this, this.__currentScope, node)); + } + + __nestFunctionScope(node, isMethodDefinition) { + return this.__nestScope(new FunctionScope(this, this.__currentScope, node, isMethodDefinition)); + } + + __nestForScope(node) { + return this.__nestScope(new ForScope(this, this.__currentScope, node)); + } + + __nestCatchScope(node) { + return this.__nestScope(new CatchScope(this, this.__currentScope, node)); + } + + __nestWithScope(node) { + return this.__nestScope(new WithScope(this, this.__currentScope, node)); + } + + __nestClassScope(node) { + return this.__nestScope(new ClassScope(this, this.__currentScope, node)); + } + + __nestClassFieldInitializerScope(node) { + return this.__nestScope(new ClassFieldInitializerScope(this, this.__currentScope, node)); + } + + __nestClassStaticBlockScope(node) { + return this.__nestScope(new ClassStaticBlockScope(this, this.__currentScope, node)); + } + + __nestSwitchScope(node) { + return this.__nestScope(new SwitchScope(this, this.__currentScope, node)); + } + + __nestModuleScope(node) { + return this.__nestScope(new ModuleScope(this, this.__currentScope, node)); + } + + __nestFunctionExpressionNameScope(node) { + return this.__nestScope(new FunctionExpressionNameScope(this, this.__currentScope, node)); + } + + __isES6() { + return this.__options.ecmaVersion >= 6; + } +} + +export default ScopeManager; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/scope.js b/packages/eslint-scope/lib/scope.js new file mode 100644 index 00000000..6c3f3cfe --- /dev/null +++ b/packages/eslint-scope/lib/scope.js @@ -0,0 +1,793 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import estraverse from "estraverse"; + +import Reference from "./reference.js"; +import Variable from "./variable.js"; +import { Definition } from "./definition.js"; +import assert from "node:assert"; + +const { Syntax } = estraverse; + +/** + * Test if scope is struct + * @param {Scope} scope scope + * @param {Block} block block + * @param {boolean} isMethodDefinition is method definition + * @returns {boolean} is strict scope + */ +function isStrictScope(scope, block, isMethodDefinition) { + let body; + + // When upper scope is exists and strict, inner scope is also strict. + if (scope.upper && scope.upper.isStrict) { + return true; + } + + if (isMethodDefinition) { + return true; + } + + if (scope.type === "class" || scope.type === "module") { + return true; + } + + if (scope.type === "block" || scope.type === "switch") { + return false; + } + + if (scope.type === "function") { + if (block.type === Syntax.ArrowFunctionExpression && block.body.type !== Syntax.BlockStatement) { + return false; + } + + if (block.type === Syntax.Program) { + body = block; + } else { + body = block.body; + } + + if (!body) { + return false; + } + } else if (scope.type === "global") { + body = block; + } else { + return false; + } + + // Search for a 'use strict' directive. + for (let i = 0, iz = body.body.length; i < iz; ++i) { + const stmt = body.body[i]; + + /* + * Check if the current statement is a directive. + * If it isn't, then we're past the directive prologue + * so stop the search because directives cannot + * appear after this point. + * + * Some parsers set `directive:null` on non-directive + * statements, so the `typeof` check is safer than + * checking for property existence. + */ + if (typeof stmt.directive !== "string") { + break; + } + + if (stmt.directive === "use strict") { + return true; + } + } + + return false; +} + +/** + * Register scope + * @param {ScopeManager} scopeManager scope manager + * @param {Scope} scope scope + * @returns {void} + */ +function registerScope(scopeManager, scope) { + scopeManager.scopes.push(scope); + + const scopes = scopeManager.__nodeToScope.get(scope.block); + + if (scopes) { + scopes.push(scope); + } else { + scopeManager.__nodeToScope.set(scope.block, [scope]); + } +} + +/** + * Should be statically + * @param {Object} def def + * @returns {boolean} should be statically + */ +function shouldBeStatically(def) { + return ( + (def.type === Variable.ClassName) || + (def.type === Variable.Variable && def.parent.kind !== "var") + ); +} + +/** + * @constructor Scope + */ +class Scope { + constructor(scopeManager, type, upperScope, block, isMethodDefinition) { + + /** + * One of "global", "module", "function", "function-expression-name", "block", "switch", "catch", "with", "for", + * "class", "class-field-initializer", "class-static-block". + * @member {string} Scope#type + */ + this.type = type; + + /** + * The scoped {@link Variable}s of this scope, as { Variable.name + * : Variable }. + * @member {Map} Scope#set + */ + this.set = new Map(); + + /** + * The tainted variables of this scope, as { Variable.name : + * boolean }. + * @member {Map} Scope#taints + */ + this.taints = new Map(); + + /** + * Generally, through the lexical scoping of JS you can always know + * which variable an identifier in the source code refers to. There are + * a few exceptions to this rule. With 'global' and 'with' scopes you + * can only decide at runtime which variable a reference refers to. + * Moreover, if 'eval()' is used in a scope, it might introduce new + * bindings in this or its parent scopes. + * All those scopes are considered 'dynamic'. + * @member {boolean} Scope#dynamic + */ + this.dynamic = this.type === "global" || this.type === "with"; + + /** + * A reference to the scope-defining syntax node. + * @member {espree.Node} Scope#block + */ + this.block = block; + + /** + * The {@link Reference|references} that are not resolved with this scope. + * @member {Reference[]} Scope#through + */ + this.through = []; + + /** + * The scoped {@link Variable}s of this scope. In the case of a + * 'function' scope this includes the automatic argument arguments as + * its first element, as well as all further formal arguments. + * @member {Variable[]} Scope#variables + */ + this.variables = []; + + /** + * Any variable {@link Reference|reference} found in this scope. This + * includes occurrences of local variables as well as variables from + * parent scopes (including the global scope). For local variables + * this also includes defining occurrences (like in a 'var' statement). + * In a 'function' scope this does not include the occurrences of the + * formal parameter in the parameter list. + * @member {Reference[]} Scope#references + */ + this.references = []; + + /** + * For 'global' and 'function' scopes, this is a self-reference. For + * other scope types this is the variableScope value of the + * parent scope. + * @member {Scope} Scope#variableScope + */ + this.variableScope = + this.type === "global" || + this.type === "module" || + this.type === "function" || + this.type === "class-field-initializer" || + this.type === "class-static-block" + ? this + : upperScope.variableScope; + + /** + * Whether this scope is created by a FunctionExpression. + * @member {boolean} Scope#functionExpressionScope + */ + this.functionExpressionScope = false; + + /** + * Whether this is a scope that contains an 'eval()' invocation. + * @member {boolean} Scope#directCallToEvalScope + */ + this.directCallToEvalScope = false; + + /** + * @member {boolean} Scope#thisFound + */ + this.thisFound = false; + + this.__left = []; + + /** + * Reference to the parent {@link Scope|scope}. + * @member {Scope} Scope#upper + */ + this.upper = upperScope; + + /** + * Whether 'use strict' is in effect in this scope. + * @member {boolean} Scope#isStrict + */ + this.isStrict = scopeManager.isStrictModeSupported() + ? isStrictScope(this, block, isMethodDefinition) + : false; + + /** + * List of nested {@link Scope}s. + * @member {Scope[]} Scope#childScopes + */ + this.childScopes = []; + if (this.upper) { + this.upper.childScopes.push(this); + } + + this.__declaredVariables = scopeManager.__declaredVariables; + + registerScope(scopeManager, this); + } + + __shouldStaticallyClose(scopeManager) { + return (!this.dynamic || scopeManager.__isOptimistic()); + } + + __shouldStaticallyCloseForGlobal(ref) { + + // On global scope, let/const/class declarations should be resolved statically. + const name = ref.identifier.name; + + if (!this.set.has(name)) { + return false; + } + + const variable = this.set.get(name); + const defs = variable.defs; + + return defs.length > 0 && defs.every(shouldBeStatically); + } + + __staticCloseRef(ref) { + if (!this.__resolve(ref)) { + this.__delegateToUpperScope(ref); + } + } + + __dynamicCloseRef(ref) { + + // notify all names are through to global + let current = this; + + do { + current.through.push(ref); + current = current.upper; + } while (current); + } + + __globalCloseRef(ref) { + + // let/const/class declarations should be resolved statically. + // others should be resolved dynamically. + if (this.__shouldStaticallyCloseForGlobal(ref)) { + this.__staticCloseRef(ref); + } else { + this.__dynamicCloseRef(ref); + } + } + + __close(scopeManager) { + let closeRef; + + if (this.__shouldStaticallyClose(scopeManager)) { + closeRef = this.__staticCloseRef; + } else if (this.type !== "global") { + closeRef = this.__dynamicCloseRef; + } else { + closeRef = this.__globalCloseRef; + } + + // Try Resolving all references in this scope. + for (let i = 0, iz = this.__left.length; i < iz; ++i) { + const ref = this.__left[i]; + + closeRef.call(this, ref); + } + this.__left = null; + + return this.upper; + } + + // To override by function scopes. + // References in default parameters isn't resolved to variables which are in their function body. + __isValidResolution(ref, variable) { // eslint-disable-line class-methods-use-this, no-unused-vars -- Desired as instance method with signature + return true; + } + + __resolve(ref) { + const name = ref.identifier.name; + + if (!this.set.has(name)) { + return false; + } + const variable = this.set.get(name); + + if (!this.__isValidResolution(ref, variable)) { + return false; + } + variable.references.push(ref); + variable.stack = variable.stack && ref.from.variableScope === this.variableScope; + if (ref.tainted) { + variable.tainted = true; + this.taints.set(variable.name, true); + } + ref.resolved = variable; + + return true; + } + + __delegateToUpperScope(ref) { + if (this.upper) { + this.upper.__left.push(ref); + } + this.through.push(ref); + } + + __addDeclaredVariablesOfNode(variable, node) { + if (node === null || node === void 0) { + return; + } + + let variables = this.__declaredVariables.get(node); + + if (variables === null || variables === void 0) { + variables = []; + this.__declaredVariables.set(node, variables); + } + if (!variables.includes(variable)) { + variables.push(variable); + } + } + + __defineGeneric(name, set, variables, node, def) { + let variable; + + variable = set.get(name); + if (!variable) { + variable = new Variable(name, this); + set.set(name, variable); + variables.push(variable); + } + + if (def) { + variable.defs.push(def); + this.__addDeclaredVariablesOfNode(variable, def.node); + this.__addDeclaredVariablesOfNode(variable, def.parent); + } + if (node) { + variable.identifiers.push(node); + } + } + + __define(node, def) { + if (node && node.type === Syntax.Identifier) { + this.__defineGeneric( + node.name, + this.set, + this.variables, + node, + def + ); + } + } + + __referencing(node, assign, writeExpr, maybeImplicitGlobal, partial, init) { + + // because Array element may be null + if (!node || node.type !== Syntax.Identifier) { + return; + } + + // Specially handle like `this`. + if (node.name === "super") { + return; + } + + const ref = new Reference(node, this, assign || Reference.READ, writeExpr, maybeImplicitGlobal, !!partial, !!init); + + this.references.push(ref); + this.__left.push(ref); + } + + __detectEval() { + let current = this; + + this.directCallToEvalScope = true; + do { + current.dynamic = true; + current = current.upper; + } while (current); + } + + __detectThis() { + this.thisFound = true; + } + + __isClosed() { + return this.__left === null; + } + + /** + * returns resolved {Reference} + * @function Scope#resolve + * @param {Espree.Identifier} ident identifier to be resolved. + * @returns {Reference} reference + */ + resolve(ident) { + let ref, i, iz; + + assert(this.__isClosed(), "Scope should be closed."); + assert(ident.type === Syntax.Identifier, "Target should be identifier."); + for (i = 0, iz = this.references.length; i < iz; ++i) { + ref = this.references[i]; + if (ref.identifier === ident) { + return ref; + } + } + return null; + } + + /** + * returns this scope is static + * @function Scope#isStatic + * @returns {boolean} static + */ + isStatic() { + return !this.dynamic; + } + + /** + * returns this scope has materialized arguments + * @function Scope#isArgumentsMaterialized + * @returns {boolean} arguemnts materialized + */ + isArgumentsMaterialized() { // eslint-disable-line class-methods-use-this -- Desired as instance method + return true; + } + + /** + * returns this scope has materialized `this` reference + * @function Scope#isThisMaterialized + * @returns {boolean} this materialized + */ + isThisMaterialized() { // eslint-disable-line class-methods-use-this -- Desired as instance method + return true; + } + + isUsedName(name) { + if (this.set.has(name)) { + return true; + } + for (let i = 0, iz = this.through.length; i < iz; ++i) { + if (this.through[i].identifier.name === name) { + return true; + } + } + return false; + } +} + +/** + * Global scope. + */ +class GlobalScope extends Scope { + constructor(scopeManager, block) { + super(scopeManager, "global", null, block, false); + this.implicit = { + set: new Map(), + variables: [], + + /** + * List of {@link Reference}s that are left to be resolved (i.e. which + * need to be linked to the variable they refer to). + * @member {Reference[]} Scope#implicit#left + */ + left: [] + }; + } + + __close(scopeManager) { + const implicit = []; + + for (let i = 0, iz = this.__left.length; i < iz; ++i) { + const ref = this.__left[i]; + + if (ref.__maybeImplicitGlobal && !this.set.has(ref.identifier.name)) { + implicit.push(ref.__maybeImplicitGlobal); + } + } + + // create an implicit global variable from assignment expression + for (let i = 0, iz = implicit.length; i < iz; ++i) { + const info = implicit[i]; + + this.__defineImplicit(info.pattern, + new Definition( + Variable.ImplicitGlobalVariable, + info.pattern, + info.node, + null, + null, + null + )); + + } + + this.implicit.left = this.__left; + + return super.__close(scopeManager); + } + + __defineImplicit(node, def) { + if (node && node.type === Syntax.Identifier) { + this.__defineGeneric( + node.name, + this.implicit.set, + this.implicit.variables, + node, + def + ); + } + } +} + +/** + * Module scope. + */ +class ModuleScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "module", upperScope, block, false); + } +} + +/** + * Function expression name scope. + */ +class FunctionExpressionNameScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "function-expression-name", upperScope, block, false); + this.__define(block.id, + new Definition( + Variable.FunctionName, + block.id, + block, + null, + null, + null + )); + this.functionExpressionScope = true; + } +} + +/** + * Catch scope. + */ +class CatchScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "catch", upperScope, block, false); + } +} + +/** + * With statement scope. + */ +class WithScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "with", upperScope, block, false); + } + + __close(scopeManager) { + if (this.__shouldStaticallyClose(scopeManager)) { + return super.__close(scopeManager); + } + + for (let i = 0, iz = this.__left.length; i < iz; ++i) { + const ref = this.__left[i]; + + ref.tainted = true; + this.__delegateToUpperScope(ref); + } + this.__left = null; + + return this.upper; + } +} + +/** + * Block scope. + */ +class BlockScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "block", upperScope, block, false); + } +} + +/** + * Switch scope. + */ +class SwitchScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "switch", upperScope, block, false); + } +} + +/** + * Function scope. + */ +class FunctionScope extends Scope { + constructor(scopeManager, upperScope, block, isMethodDefinition) { + super(scopeManager, "function", upperScope, block, isMethodDefinition); + + // section 9.2.13, FunctionDeclarationInstantiation. + // NOTE Arrow functions never have an arguments objects. + if (this.block.type !== Syntax.ArrowFunctionExpression) { + this.__defineArguments(); + } + } + + isArgumentsMaterialized() { + + // TODO(Constellation) + // We can more aggressive on this condition like this. + // + // function t() { + // // arguments of t is always hidden. + // function arguments() { + // } + // } + if (this.block.type === Syntax.ArrowFunctionExpression) { + return false; + } + + if (!this.isStatic()) { + return true; + } + + const variable = this.set.get("arguments"); + + assert(variable, "Always have arguments variable."); + return variable.tainted || variable.references.length !== 0; + } + + isThisMaterialized() { + if (!this.isStatic()) { + return true; + } + return this.thisFound; + } + + __defineArguments() { + this.__defineGeneric( + "arguments", + this.set, + this.variables, + null, + null + ); + this.taints.set("arguments", true); + } + + // References in default parameters isn't resolved to variables which are in their function body. + // const x = 1 + // function f(a = x) { // This `x` is resolved to the `x` in the outer scope. + // const x = 2 + // console.log(a) + // } + __isValidResolution(ref, variable) { + + // If `options.nodejsScope` is true, `this.block` becomes a Program node. + if (this.block.type === "Program") { + return true; + } + + const bodyStart = this.block.body.range[0]; + + // It's invalid resolution in the following case: + return !( + variable.scope === this && + ref.identifier.range[0] < bodyStart && // the reference is in the parameter part. + variable.defs.every(d => d.name.range[0] >= bodyStart) // the variable is in the body. + ); + } +} + +/** + * Scope of for, for-in, and for-of statements. + */ +class ForScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "for", upperScope, block, false); + } +} + +/** + * Class scope. + */ +class ClassScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "class", upperScope, block, false); + } +} + +/** + * Class field initializer scope. + */ +class ClassFieldInitializerScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "class-field-initializer", upperScope, block, true); + } +} + +/** + * Class static block scope. + */ +class ClassStaticBlockScope extends Scope { + constructor(scopeManager, upperScope, block) { + super(scopeManager, "class-static-block", upperScope, block, true); + } +} + +export { + Scope, + GlobalScope, + ModuleScope, + FunctionExpressionNameScope, + CatchScope, + WithScope, + BlockScope, + SwitchScope, + FunctionScope, + ForScope, + ClassScope, + ClassFieldInitializerScope, + ClassStaticBlockScope +}; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/variable.js b/packages/eslint-scope/lib/variable.js new file mode 100644 index 00000000..286202f7 --- /dev/null +++ b/packages/eslint-scope/lib/variable.js @@ -0,0 +1,87 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * A Variable represents a locally scoped identifier. These include arguments to + * functions. + * @constructor Variable + */ +class Variable { + constructor(name, scope) { + + /** + * The variable name, as given in the source code. + * @member {string} Variable#name + */ + this.name = name; + + /** + * List of defining occurrences of this variable (like in 'var ...' + * statements or as parameter), as AST nodes. + * @member {espree.Identifier[]} Variable#identifiers + */ + this.identifiers = []; + + /** + * List of {@link Reference|references} of this variable (excluding parameter entries) + * in its defining scope and all nested scopes. For defining + * occurrences only see {@link Variable#defs}. + * @member {Reference[]} Variable#references + */ + this.references = []; + + /** + * List of defining occurrences of this variable (like in 'var ...' + * statements or as parameter), as custom objects. + * @member {Definition[]} Variable#defs + */ + this.defs = []; + + this.tainted = false; + + /** + * Whether this is a stack variable. + * @member {boolean} Variable#stack + */ + this.stack = true; + + /** + * Reference to the enclosing Scope. + * @member {Scope} Variable#scope + */ + this.scope = scope; + } +} + +Variable.CatchClause = "CatchClause"; +Variable.Parameter = "Parameter"; +Variable.FunctionName = "FunctionName"; +Variable.ClassName = "ClassName"; +Variable.Variable = "Variable"; +Variable.ImportBinding = "ImportBinding"; +Variable.ImplicitGlobalVariable = "ImplicitGlobalVariable"; + +export default Variable; + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/lib/version.js b/packages/eslint-scope/lib/version.js new file mode 100644 index 00000000..9e460f71 --- /dev/null +++ b/packages/eslint-scope/lib/version.js @@ -0,0 +1,12 @@ +/** + * @fileoverview Contains the current version number. This file is checked in + * to source control and should always return "main". During the release + * process, this file is overwritten with a new on that returns the actual + * version number. The file with the actual version number is *not* checked + * back into the repo. This is only necessary until Node.js supports import + * assertions, at which time we can just import package.json for this info. + * @author Nicholas C. Zakas + */ +const version = "main"; + +export default version; diff --git a/packages/eslint-scope/licenses-meta-data.json b/packages/eslint-scope/licenses-meta-data.json new file mode 100644 index 00000000..f6e38bb6 --- /dev/null +++ b/packages/eslint-scope/licenses-meta-data.json @@ -0,0 +1,10 @@ +{ + "esrecurse@4.3.0": { + "licenses": "BSD-2-Clause", + "repository": "https://github.com/estools/esrecurse" + }, + "estraverse@5.3.0": { + "licenses": "BSD-2-Clause", + "repository": "https://github.com/estools/estraverse" + } +} diff --git a/packages/eslint-scope/package.json b/packages/eslint-scope/package.json new file mode 100644 index 00000000..60d2283e --- /dev/null +++ b/packages/eslint-scope/package.json @@ -0,0 +1,59 @@ +{ + "name": "eslint-scope", + "description": "ECMAScript scope analyzer for ESLint", + "homepage": "http://github.com/eslint/eslint-scope", + "main": "./dist/eslint-scope.cjs", + "type": "module", + "exports": { + ".": { + "import": "./lib/index.js", + "require": "./dist/eslint-scope.cjs" + }, + "./package.json": "./package.json" + }, + "version": "8.0.2", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "repository": "eslint/eslint-scope", + "funding": "https://opencollective.com/eslint", + "bugs": { + "url": "https://github.com/eslint/eslint-scope/issues" + }, + "license": "BSD-2-Clause", + "scripts": { + "build": "rollup -c", + "build:update-version": "node tools/update-version.js", + "prepublishOnly": "npm run build:update-version && npm run build", + "pretest": "npm run build", + "release:generate:latest": "eslint-generate-release", + "release:generate:alpha": "eslint-generate-prerelease alpha", + "release:generate:beta": "eslint-generate-prerelease beta", + "release:generate:rc": "eslint-generate-prerelease rc", + "release:publish": "eslint-publish-release", + "test": "node Makefile.js test" + }, + "files": [ + "LICENSE", + "README.md", + "lib", + "dist/eslint-scope.cjs" + ], + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "devDependencies": { + "@typescript-eslint/parser": "^7.1.1", + "c8": "^7.7.3", + "chai": "^4.3.4", + "eslint-release": "^3.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", + "mocha": "^9.0.1", + "npm-license": "^0.3.3", + "rollup": "^2.52.7", + "shelljs": "^0.8.5", + "typescript": "^5.4.2" + } +} diff --git a/packages/eslint-scope/rollup.config.js b/packages/eslint-scope/rollup.config.js new file mode 100644 index 00000000..135e147a --- /dev/null +++ b/packages/eslint-scope/rollup.config.js @@ -0,0 +1,10 @@ +export default { + input: "./lib/index.js", + external: ["assert", "estraverse", "esrecurse"], + treeshake: false, + output: { + format: "cjs", + file: "dist/eslint-scope.cjs", + sourcemap: true + } +}; diff --git a/packages/eslint-scope/tests/arguments.js b/packages/eslint-scope/tests/arguments.js new file mode 100644 index 00000000..6616160e --- /dev/null +++ b/packages/eslint-scope/tests/arguments.js @@ -0,0 +1,56 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("arguments", () => { + it("arguments are correctly materialized", () => { + const ast = espree(` + (function () { + arguments; + }()); + `); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.isArgumentsMaterialized()).to.be.true; + expect(scope.references).to.have.length(1); + expect(scope.references[0].resolved).to.be.equal(scope.variables[0]); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/catch-scope.js b/packages/eslint-scope/tests/catch-scope.js new file mode 100644 index 00000000..d425468b --- /dev/null +++ b/packages/eslint-scope/tests/catch-scope.js @@ -0,0 +1,151 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("catch", () => { + it("creates scope", () => { + const ast = espree(` + (function () { + try { + } catch (e) { + } + }()); + `); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(3); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + let scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.isArgumentsMaterialized()).to.be.false; + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("catch"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("e"); + expect(scope.variables[0].defs).to.have.length(1); + expect(scope.variables[0].defs[0].type).to.be.equal("CatchClause"); + expect(scope.variables[0].defs[0].name.type).to.be.equal("Identifier"); + expect(scope.variables[0].defs[0].name.name).to.be.equal("e"); + expect(scope.variables[0].defs[0].node.type).to.be.equal("CatchClause"); + expect(scope.variables[0].defs[0].parent).to.be.equal(null); + expect(scope.isArgumentsMaterialized()).to.be.true; + expect(scope.references).to.have.length(0); + }); + + it("param can be a pattern", () => { + const ast = espree(` + (function () { + const default_id = 0; + try { + } catch ({ message, id = default_id, args: [arg1, arg2] }) { + } + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(5); + + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const functionScope = scopeManager.scopes[1]; + + expect(functionScope.type).to.be.equal("function"); + expect(functionScope.variables).to.have.length(2); + expect(functionScope.variables[0].name).to.be.equal("arguments"); + expect(functionScope.variables[1].name).to.be.equal("default_id"); + expect(functionScope.references).to.have.length(1); + expect(functionScope.references[0].from).to.be.equal(functionScope); + expect(functionScope.references[0].resolved).to.be.equal(functionScope.variables[1]); + + const tryBlockScope = scopeManager.scopes[2]; + + expect(tryBlockScope.type).to.be.equal("block"); + expect(tryBlockScope.variables).to.have.length(0); + expect(tryBlockScope.references).to.have.length(0); + + const catchScope = scopeManager.scopes[3]; + + expect(catchScope.type).to.be.equal("catch"); + expect(catchScope.variables).to.have.length(4); + expect(catchScope.variables[0].name).to.be.equal("message"); + expect(catchScope.variables[0].defs).to.have.length(1); + expect(catchScope.variables[0].defs[0].type).to.be.equal("CatchClause"); + expect(catchScope.variables[0].defs[0].name.type).to.be.equal("Identifier"); + expect(catchScope.variables[0].defs[0].name.name).to.be.equal("message"); + expect(catchScope.variables[0].defs[0].node.type).to.be.equal("CatchClause"); + expect(catchScope.variables[0].defs[0].parent).to.be.equal(null); + expect(catchScope.variables[1].name).to.be.equal("id"); + expect(catchScope.variables[1].defs).to.have.length(1); + expect(catchScope.variables[1].defs[0].type).to.be.equal("CatchClause"); + expect(catchScope.variables[1].defs[0].name.type).to.be.equal("Identifier"); + expect(catchScope.variables[1].defs[0].name.name).to.be.equal("id"); + expect(catchScope.variables[1].defs[0].node.type).to.be.equal("CatchClause"); + expect(catchScope.variables[1].defs[0].parent).to.be.equal(null); + expect(catchScope.variables[2].name).to.be.equal("arg1"); + expect(catchScope.variables[2].defs).to.have.length(1); + expect(catchScope.variables[2].defs[0].type).to.be.equal("CatchClause"); + expect(catchScope.variables[2].defs[0].name.type).to.be.equal("Identifier"); + expect(catchScope.variables[2].defs[0].name.name).to.be.equal("arg1"); + expect(catchScope.variables[2].defs[0].node.type).to.be.equal("CatchClause"); + expect(catchScope.variables[2].defs[0].parent).to.be.equal(null); + expect(catchScope.variables[3].name).to.be.equal("arg2"); + expect(catchScope.variables[3].defs).to.have.length(1); + expect(catchScope.variables[3].defs[0].type).to.be.equal("CatchClause"); + expect(catchScope.variables[3].defs[0].name.type).to.be.equal("Identifier"); + expect(catchScope.variables[3].defs[0].name.name).to.be.equal("arg2"); + expect(catchScope.variables[3].defs[0].node.type).to.be.equal("CatchClause"); + expect(catchScope.variables[3].defs[0].parent).to.be.equal(null); + expect(catchScope.references).to.have.length(2); + expect(catchScope.references[0].from).to.be.equal(catchScope); + expect(catchScope.references[0].resolved).to.be.equal(catchScope.variables[1]); + expect(catchScope.references[1].from).to.be.equal(catchScope); + expect(catchScope.references[1].resolved).to.be.equal(functionScope.variables[1]); + + const catchBlockScope = scopeManager.scopes[4]; + + expect(catchBlockScope.type).to.be.equal("block"); + expect(catchBlockScope.variables).to.have.length(0); + expect(catchBlockScope.references).to.have.length(0); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/child-visitor-keys.js b/packages/eslint-scope/tests/child-visitor-keys.js new file mode 100644 index 00000000..d7004246 --- /dev/null +++ b/packages/eslint-scope/tests/child-visitor-keys.js @@ -0,0 +1,102 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2016 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("childVisitorKeys option", () => { + it("should handle as a known node if the childVisitorKeys option was given.", () => { + const ast = espree(` + var foo = 0; + `); + + ast.body[0].declarations[0].init.type = "NumericLiteral"; + + // should no error + analyze( + ast, + { + fallback: "none", + childVisitorKeys: { + NumericLiteral: [] + } + } + ); + }); + + it("should not visit to properties which are not given.", () => { + const ast = espree(` + let foo = bar; + `); + + ast.body[0].declarations[0].init = { + type: "TestNode", + argument: ast.body[0].declarations[0].init + }; + + const result = analyze( + ast, + { + childVisitorKeys: { + TestNode: [] + } + } + ); + + expect(result.scopes).to.have.length(1); + const globalScope = result.scopes[0]; + + // `bar` in TestNode has not been visited. + expect(globalScope.through).to.have.length(0); + }); + + it("should visit to given properties.", () => { + const ast = espree(` + let foo = bar; + `); + + ast.body[0].declarations[0].init = { + type: "TestNode", + argument: ast.body[0].declarations[0].init + }; + + const result = analyze( + ast, + { + childVisitorKeys: { + TestNode: ["argument"] + } + } + ); + + expect(result.scopes).to.have.length(1); + const globalScope = result.scopes[0]; + + // `bar` in TestNode has been visited. + expect(globalScope.through).to.have.length(1); + expect(globalScope.through[0].identifier.name).to.equal("bar"); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/class-fields.js b/packages/eslint-scope/tests/class-fields.js new file mode 100644 index 00000000..5d23821e --- /dev/null +++ b/packages/eslint-scope/tests/class-fields.js @@ -0,0 +1,303 @@ +/** + * @fileoverview Tests for class fields syntax. + * @author Toru Nagashima + */ + +import assert from "node:assert"; +import * as espree from "espree"; +import { KEYS } from "eslint-visitor-keys"; +import { analyze } from "../lib/index.js"; + +describe("Class fields", () => { + describe("class C { f = g }", () => { + let scopes; + + beforeEach(() => { + const ast = espree.parse("class C { f = g }", { ecmaVersion: 13 }); + const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + scopes = manager.globalScope.childScopes; + }); + + it("should create a class scope.", () => { + assert.strictEqual(scopes.length, 1); + assert.strictEqual(scopes[0].type, "class"); + }); + + it("The class scope has no references.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("The class scope has only the variable 'C'; it doesn't have the field name 'f'.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.variables.length, 1); + assert.strictEqual(classScope.variables[0].name, "C"); + }); + + it("The class scope has a class-field-initializer scope.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + }); + + it("The class-field-initializer scope's block is the node of the field initializer.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.block.type, "Identifier"); + assert.strictEqual(fieldInitializerScope.block.name, "g"); + }); + + it("The class-field-initializer scope's variableScope is itself.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variableScope, fieldInitializerScope); + }); + + it("The class-field-initializer scope has only the reference 'g'.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.references.length, 1); + assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "g"); + }); + + it("The class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + }); + + describe("class C { f }", () => { + let scopes; + + beforeEach(() => { + const ast = espree.parse("class C { f }", { ecmaVersion: 13 }); + const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + scopes = manager.globalScope.childScopes; + }); + + it("should create a class scope.", () => { + assert.strictEqual(scopes.length, 1); + assert.strictEqual(scopes[0].type, "class"); + }); + + it("The class scope has no references.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("The class scope has no child scopes; fields that don't have initializers don't create any class-field-initializer scopes.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 0); + }); + + it("The class scope has only the variable 'C'; it doesn't have the field name 'f'.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.variables.length, 1); + assert.strictEqual(classScope.variables[0].name, "C"); + }); + }); + + describe("class C { #f = g }", () => { + let scopes; + + beforeEach(() => { + const ast = espree.parse("class C { #f = g }", { ecmaVersion: 13 }); + const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + scopes = manager.globalScope.childScopes; + }); + + it("should create a class scope.", () => { + assert.strictEqual(scopes.length, 1); + assert.strictEqual(scopes[0].type, "class"); + }); + + it("The class scope has no references.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("The class scope has only the variable 'C'; it doesn't have the field name '#f'.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.variables.length, 1); + assert.strictEqual(classScope.variables[0].name, "C"); + }); + + it("The class scope has a class-field-initializer scope.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + }); + + it("The class-field-initializer scope has only the reference 'g'.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.references.length, 1); + assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "g"); + }); + + it("The class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + }); + + describe("class C { [fname] }", () => { + let scopes; + + beforeEach(() => { + const ast = espree.parse("class C { [fname] }", { ecmaVersion: 13 }); + const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + scopes = manager.globalScope.childScopes; + }); + + it("should create a class scope.", () => { + assert.strictEqual(scopes.length, 1); + assert.strictEqual(scopes[0].type, "class"); + }); + + it("The class scope has only the reference 'fname'.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.references.length, 1); + assert.strictEqual(classScope.references[0].identifier.name, "fname"); + }); + + it("The class scope has no child scopes; fields that don't have initializers don't create any class-field-initializer scopes.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 0); + }); + }); + + describe("class C { [fname] = value }", () => { + let scopes; + + beforeEach(() => { + const ast = espree.parse("class C { [fname] = value }", { ecmaVersion: 13 }); + const manager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + scopes = manager.globalScope.childScopes; + }); + + it("should create a class scope.", () => { + assert.strictEqual(scopes.length, 1); + assert.strictEqual(scopes[0].type, "class"); + }); + + it("The class scope has only the reference 'fname'; it doesn't have the reference 'value'.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.references.length, 1); + assert.strictEqual(classScope.references[0].identifier.name, "fname"); + }); + + it("The class scope has a class-field-initializer scope.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + }); + + it("The class-field-initializer scope has the reference 'value'.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.references.length, 1); + assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "value"); + }); + + it("The class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + }); + + describe("class C { #f = g; e = this.#f }", () => { + let scopes; + + beforeEach(() => { + const ast = espree.parse("class C { #f = g; e = this.#f }", { ecmaVersion: 13 }); + const manager = analyze(ast, { ecmaVersion: 13, childVistorKeys: KEYS }); + + scopes = manager.globalScope.childScopes; + }); + + it("should create a class scope.", () => { + assert.strictEqual(scopes.length, 1); + assert.strictEqual(scopes[0].type, "class"); + }); + + it("The class scope has no references.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("The class scope has only the variable 'C'; it doesn't have the field names '#f' or 'e'.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.variables.length, 1); + assert.strictEqual(classScope.variables[0].name, "C"); + }); + + it("The class scope has two class-field-initializer scopes.", () => { + const classScope = scopes[0]; + + assert.strictEqual(classScope.childScopes.length, 2); + assert.strictEqual(classScope.childScopes[0].type, "class-field-initializer"); + assert.strictEqual(classScope.childScopes[1].type, "class-field-initializer"); + }); + + it("The first class-field-initializer scope has only the reference 'g'.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.references.length, 1); + assert.strictEqual(fieldInitializerScope.references[0].identifier.name, "g"); + }); + + it("The first class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[0]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + + it("The second class-field-initializer scope has no references.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[1]; + + assert.strictEqual(fieldInitializerScope.references.length, 0); + }); + + it("The second class-field-initializer scope has no variables.", () => { + const classScope = scopes[0]; + const fieldInitializerScope = classScope.childScopes[1]; + + assert.strictEqual(fieldInitializerScope.variables.length, 0); + }); + }); +}); diff --git a/packages/eslint-scope/tests/class-static-blocks.js b/packages/eslint-scope/tests/class-static-blocks.js new file mode 100644 index 00000000..1a9f9d76 --- /dev/null +++ b/packages/eslint-scope/tests/class-static-blocks.js @@ -0,0 +1,875 @@ +/** + * @fileoverview Tests for class static blocks. + * @author Milos Djermanovic + */ + +import assert from "node:assert"; +import * as espree from "espree"; +import { KEYS } from "eslint-visitor-keys"; +import { analyze } from "../lib/index.js"; + +describe("Class static blocks", () => { + + describe("class C { static { var a; let b; const c = 1; function d(){} class e {} } }", () => { + let ast; + let scopeManager; + let globalScope; + + beforeEach(() => { + ast = espree.parse("class C { static { var a; let b; const c = 1; function d(){} class e {} } }", { ecmaVersion: 13 }); + scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + ({ globalScope } = scopeManager); + }); + + it("the global scope should have one variable, named `C`", () => { + assert.strictEqual(globalScope.variables.length, 1); + assert.strictEqual(globalScope.variables[0].name, "C"); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have one variable, named `C`", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.variables.length, 1); + assert.strictEqual(classScope.variables[0].name, "C"); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have two child scopes, a function and a class scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 2); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "function"); + assert.strictEqual(classStaticBlockScope.childScopes[0].upper, classStaticBlockScope); + assert.strictEqual(classStaticBlockScope.childScopes[1].type, "class"); + assert.strictEqual(classStaticBlockScope.childScopes[1].upper, classStaticBlockScope); + }); + + it("the class static block scope's `upper` scope should be the class scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.upper, classScope); + }); + + it("the class static block scope's `variableScope` is itself", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variableScope, classStaticBlockScope); + }); + + it("the class static block scope should be strict", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.isStrict, true); + }); + + it("the class static block scope's `functionExpressionScope` property should have value `false`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.functionExpressionScope, false); + }); + + it("the class static block scope's `block` node is the `StaticBlock` node from the AST", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const staticBlockNode = ast.body[0].body.body[0]; + + assert.strictEqual(staticBlockNode.type, "StaticBlock"); + assert.strictEqual(classStaticBlockScope.block, staticBlockNode); + }); + + it("the class static block scope should be returned by ScopeManager#acquire for the `StaticBlock` node from the AST", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const staticBlockNode = ast.body[0].body.body[0]; + + assert.strictEqual(staticBlockNode.type, "StaticBlock"); + assert.strictEqual(scopeManager.acquire(staticBlockNode, /* inner = */ false), classStaticBlockScope); + assert.strictEqual(scopeManager.acquire(staticBlockNode, /* inner = */ true), classStaticBlockScope); + }); + + it("the `StaticBlock` node itself doesn't declare any variables", () => { + const staticBlockNode = ast.body[0].body.body[0]; + + assert.strictEqual(staticBlockNode.type, "StaticBlock"); + assert.strictEqual(scopeManager.getDeclaredVariables(staticBlockNode).length, 0); + }); + + it("the class static block scope should have 5 variables: `a`, `b`, `c`, `d`, `e`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const expectedVariableNames = ["a", "b", "c", "d", "e"]; + + assert.deepStrictEqual(classStaticBlockScope.variables.map(v => v.name), expectedVariableNames); + assert.deepStrictEqual([...classStaticBlockScope.set.keys()], expectedVariableNames); + assert.deepStrictEqual([...classStaticBlockScope.set.values()], classStaticBlockScope.variables); + classStaticBlockScope.variables.forEach(variable => { + assert.strictEqual(variable.scope, classStaticBlockScope); + }); + }); + }); + + describe("class C { static { function f(){} f(); } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { function f(){} f(); } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the class static block scope should have one variable, named `f`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "f"); + }); + + it("the class static block scope should have one reference, to the variable `f`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, classStaticBlockScope.variables[0]); + }); + + it("the variable `f` should have one reference, from the class static block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const f = classStaticBlockScope.variables[0]; + + assert.strictEqual(f.references.length, 1); + assert.strictEqual(f.references[0].from, classStaticBlockScope); + assert.strictEqual(f.references[0], classStaticBlockScope.references[0]); + }); + }); + + describe("class C { static { a = 1; if (this.x) { var a; } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { a = 1; if (this.x) { var a; } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the class static block scope should have one variable, named `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "a"); + }); + + it("the class static block scope should have one reference, to the variable `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, classStaticBlockScope.variables[0]); + }); + + it("the variable `a` should have one reference, from the class static block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(a.references.length, 1); + assert.strictEqual(a.references[0].from, classStaticBlockScope); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the block scope should have no variables", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.variables.length, 0); + }); + + it("the block scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.references.length, 0); + }); + }); + + describe("class C { static { if (this.x) { var a; a = 1; } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { if (this.x) { var a; a = 1; } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the class static block scope should have one variable, named `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "a"); + }); + + it("the class static block scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 0); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the variable `a` should have one reference, from the block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(a.references.length, 1); + assert.strictEqual(a.references[0].from, blockScope); + }); + + it("the block scope should have no variables", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.variables.length, 0); + }); + + it("the block scope should have one reference, to the variable `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(blockScope.references.length, 1); + assert.strictEqual(blockScope.references[0].resolved, a); + }); + }); + + describe("class C { static { const { a } = this.foo; if (this.bar) { const b = a + 1; this.baz(b); } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { const { a } = this.foo; if (this.bar) { const b = a + 1; this.baz(b); } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the class static block scope should have one variable, named `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 1); + assert.strictEqual(classStaticBlockScope.variables[0].name, "a"); + }); + + it("the variable `a` should have a write reference from the class static block scope, and a read reference from the block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(a.references.length, 2); + assert.strictEqual(a.references[0].isWriteOnly(), true); + assert.strictEqual(a.references[0].from, classStaticBlockScope); + assert.strictEqual(a.references[1].isReadOnly(), true); + assert.strictEqual(a.references[1].from, blockScope); + }); + + it("the class static block scope should have no `through` references`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 0); + }); + + it("the block scope should have one variable, named `b`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.variables.length, 1); + assert.strictEqual(blockScope.variables[0].name, "b"); + }); + + it("the variable `b` should have a write reference and a read reference from the block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + const b = blockScope.variables[0]; + + assert.strictEqual(b.references.length, 2); + assert.strictEqual(b.references[0].isWriteOnly(), true); + assert.strictEqual(b.references[0].from, blockScope); + assert.strictEqual(b.references[1].isReadOnly(), true); + assert.strictEqual(b.references[1].from, blockScope); + }); + + it("the block scope should have one `through` reference, to the variable `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + const a = classStaticBlockScope.variables[0]; + + assert.strictEqual(blockScope.through.length, 1); + assert.strictEqual(blockScope.through[0].resolved, a); + }); + }); + + describe("class C { static { C.x; } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { C.x; } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have variable `C`", () => { + assert.strictEqual(globalScope.set.has("C"), true); + }); + + it("the global variable `C` should have no references", () => { + assert.strictEqual(globalScope.set.get("C").references.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class scope should have variable `C`", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.set.has("C"), true); + }); + + it("the class scope's variable `C` should have one reference, from the class static block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const C = classScope.set.get("C"); + + assert.strictEqual(C.references.length, 1); + assert.strictEqual(C.references[0].from, classStaticBlockScope); + }); + + it("the class static block scope should have one reference, to the class scope's variable `C`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const C = classScope.set.get("C"); + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, C); + }); + + it("the class static block scope should have one `through` reference, to the class scope's variable `C`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const C = classScope.set.get("C"); + + assert.strictEqual(classStaticBlockScope.through.length, 1); + assert.strictEqual(classStaticBlockScope.through[0].resolved, C); + }); + + it("the class static block scope should have no variables", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.variables.length, 0); + }); + }); + + describe("let a; class C { static { lbl: { this.b = a } } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("let a; class C { static { lbl: { this.b = a } } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have no references", () => { + assert.strictEqual(globalScope.references.length, 0); + }); + + it("the global scope should have no `through` references", () => { + assert.strictEqual(globalScope.through.length, 0); + }); + + it("the global scope should have variable `a`", () => { + assert.strictEqual(globalScope.set.has("a"), true); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have one `through` reference, a reference to `a`", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 1); + assert.strictEqual(classScope.through[0].resolved, globalScope.set.get("a")); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 0); + }); + + it("the class static block scope should have one `through` reference, a reference to `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.through.length, 1); + assert.strictEqual(classStaticBlockScope.through[0].resolved, globalScope.set.get("a")); + }); + + it("the class static block scope should have one child scope, a block scope", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.childScopes.length, 1); + assert.strictEqual(classStaticBlockScope.childScopes[0].type, "block"); + }); + + it("the block scope should have one reference, a reference to `a", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.references.length, 1); + assert.strictEqual(blockScope.references[0].resolved, globalScope.set.get("a")); + }); + + it("the block scope should have one `through` reference, a reference to `a", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + const blockScope = classStaticBlockScope.childScopes[0]; + + assert.strictEqual(blockScope.through.length, 1); + assert.strictEqual(blockScope.through[0].resolved, globalScope.set.get("a")); + }); + }); + + describe("class C { static { a; } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("class C { static { a; } }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have one child scope, a class static block scope", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 1); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + }); + + it("the class static block scope should have one unresolved reference, with identifier `a`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + assert.strictEqual(classStaticBlockScope.references.length, 1); + assert.strictEqual(classStaticBlockScope.references[0].resolved, null); + }); + + it("the class static block scope and all upper scopes should have the unresolved reference in `through`", () => { + const classScope = globalScope.childScopes[0]; + const classStaticBlockScope = classScope.childScopes[0]; + + const reference = classStaticBlockScope.references[0]; + + let scope = classStaticBlockScope; + + do { + assert.strictEqual(scope.through[0], reference); + scope = scope.upper; + } while (scope); + }); + }); + + describe("let a; class C { static { let a; a; } static { a; let a; } }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("let a; class C { static { let a; a; } static { a; let a; } }", { ecmaVersion: 13, range: true }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have variable `a`", () => { + assert.strictEqual(globalScope.set.has("a"), true); + }); + + it("the global scope variable `a` should not have any references", () => { + assert.strictEqual(globalScope.set.get("a").references.length, 0); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the class scope should have no references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.references.length, 0); + }); + + it("the class scope should have no `through` references", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.through.length, 0); + }); + + it("the class scope should have two child scopes, and those are two different class static block scopes", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 2); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[0].block.type, "StaticBlock"); + assert.strictEqual(classScope.childScopes[1].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[1].block.type, "StaticBlock"); + assert.ok(classScope.childScopes[0] !== classScope.childScopes[1], "Class static block scopes are same"); + assert.ok(classScope.childScopes[0].block !== classScope.childScopes[1].block, "Class static block scopes are on the same node"); + assert.ok(classScope.childScopes[0].upper === classScope.childScopes[1].upper, "Class static block scopes don't have the same upper scope"); + }); + + it("each of the two class static block scopes should have one variable `a` and one reference to that variable", () => { + const classScope = globalScope.childScopes[0]; + + classScope.childScopes.forEach(classStaticBlockScope => { + assert.strictEqual(classStaticBlockScope.variables.length, 1); + + const variable = classStaticBlockScope.variables[0]; + + assert.ok(variable.scope === classStaticBlockScope, "Variable is from another scope"); + assert.strictEqual(variable.name, "a"); + assert.strictEqual(variable.references.length, 1); + + const reference = variable.references[0]; + const referenceIdentifier = reference.identifier; + const staticBlockNode = classStaticBlockScope.block; + + assert.ok( + reference.from === classStaticBlockScope, + "Reference is from another scope" + ); + + assert.ok( + staticBlockNode.range[0] <= referenceIdentifier.range[0] && + referenceIdentifier.range[1] <= staticBlockNode.range[1], + "Reference is from another node" + ); + }); + }); + }); + + describe("let a; class C { [a]; static { let a; } [a]; static { function a(){} } [a]; static { var a; } [a]; }", () => { + let globalScope; + + beforeEach(() => { + const ast = espree.parse("let a; class C { [a]; static { let a; } [a]; static { function a(){} } [a]; static { var a; } [a]; }", { ecmaVersion: 13 }); + const scopeManager = analyze(ast, { ecmaVersion: 13, childVisitorKeys: KEYS }); + + ({ globalScope } = scopeManager); + }); + + it("the global scope should have variable `a`", () => { + assert.strictEqual(globalScope.set.has("a"), true); + }); + + it("the global scope should have one child scope, a class scope", () => { + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "class"); + }); + + it("the global variable `a` should have 4 references from the class scope", () => { + const a = globalScope.set.get("a"); + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(a.references.length, 4); + + a.references.forEach(reference => { + assert.strictEqual(reference.from, classScope); + }); + }); + + it("the class scope should have 3 child scopes, and those are class static block scopes", () => { + const classScope = globalScope.childScopes[0]; + + assert.strictEqual(classScope.childScopes.length, 3); + assert.strictEqual(classScope.childScopes[0].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[1].type, "class-static-block"); + assert.strictEqual(classScope.childScopes[2].type, "class-static-block"); + }); + + it("each of the class static block scopes should have one variable named `a` with no references", () => { + const classScope = globalScope.childScopes[0]; + + classScope.childScopes.forEach(classStaticBlockScope => { + assert.strictEqual(classStaticBlockScope.variables.length, 1); + + const variable = classStaticBlockScope.variables[0]; + + assert.strictEqual(variable.name, "a"); + assert.strictEqual(variable.references.length, 0); + }); + }); + }); +}); diff --git a/packages/eslint-scope/tests/commonjs.cjs b/packages/eslint-scope/tests/commonjs.cjs new file mode 100644 index 00000000..ccd4e4a2 --- /dev/null +++ b/packages/eslint-scope/tests/commonjs.cjs @@ -0,0 +1,41 @@ +/** + * @fileoverview Tests for checking that the commonjs entry points are still accessible + * @author Mike Reinstein + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("node:assert"); +const eslintScope = require("../dist/eslint-scope.cjs"); + + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("commonjs", () => { + it("is an object", () => { + assert.strictEqual(typeof eslintScope, "object"); + }); + + it("has exports", () => { + assert.strictEqual(typeof eslintScope.version, "string"); + + [ + "analyze", + "Definition", + "PatternVisitor", + "Reference", + "Referencer", + "Scope", + "ScopeManager", + "Variable" + ].forEach(prop => { + assert.strictEqual(typeof eslintScope[prop], "function"); + }); + }); +}); diff --git a/packages/eslint-scope/tests/es6-arrow-function-expression.js b/packages/eslint-scope/tests/es6-arrow-function-expression.js new file mode 100644 index 00000000..c47f25cc --- /dev/null +++ b/packages/eslint-scope/tests/es6-arrow-function-expression.js @@ -0,0 +1,154 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 arrow function expression", () => { + it("materialize scope for arrow function expression", () => { + const ast = espree(` + var arrow = () => { + let i = 0; + var j = 20; + console.log(i); + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("ArrowFunctionExpression"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(2); + + // There's no "arguments" + expect(scope.variables[0].name).to.be.equal("i"); + expect(scope.variables[1].name).to.be.equal("j"); + }); + + it("generate bindings for parameters", () => { + const ast = espree("var arrow = (a, b, c, d) => {}"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("ArrowFunctionExpression"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(4); + + // There's no "arguments" + expect(scope.variables[0].name).to.be.equal("a"); + expect(scope.variables[1].name).to.be.equal("b"); + expect(scope.variables[2].name).to.be.equal("c"); + expect(scope.variables[3].name).to.be.equal("d"); + }); + + it("inherits upper scope strictness", () => { + const ast = espree(` + "use strict"; + var arrow = () => {}; + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + + scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("ArrowFunctionExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(0); + }); + + it("is strict when a strictness directive is used", () => { + const ast = espree(` + var arrow = () => { + "use strict"; + }; + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + + scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("ArrowFunctionExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(0); + }); + + it("works with no body", () => { + const ast = espree("var arrow = a => a;"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("ArrowFunctionExpression"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-block-scope.js b/packages/eslint-scope/tests/es6-block-scope.js new file mode 100644 index 00000000..939003a0 --- /dev/null +++ b/packages/eslint-scope/tests/es6-block-scope.js @@ -0,0 +1,195 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { getSupportedEcmaVersions } from "./util/ecma-version.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 block scope", () => { + it("let is materialized in ES6 block scope#1", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + { + let i = 20; + i; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(2); // Program and BlockStatement scope. + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); // No variable in Program scope. + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(1); // `i` in block scope. + expect(scope.variables[0].name).to.be.equal("i"); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("i"); + expect(scope.references[1].identifier.name).to.be.equal("i"); + }); + }); + + it("function delaration is materialized in ES6 block scope", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + { + function test() { + } + test(); + } + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("test"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("test"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); + }); + }); + + it("let is not hoistable#1", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + var i = 42; (1) + { + i; // (2) ReferenceError at runtime. + let i = 20; // (2) + i; // (2) + } + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(2); + + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(1); + expect(globalScope.variables[0].name).to.be.equal("i"); + expect(globalScope.references).to.have.length(1); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("i"); + expect(scope.references).to.have.length(3); + expect(scope.references[0].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[1].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[2].resolved).to.be.equal(scope.variables[0]); + }); + + }); + + it("let is not hoistable#2", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + (function () { + var i = 42; // (1) + i; // (1) + { + i; // (3) + { + i; // (2) + let i = 20; // (2) + i; // (2) + } + let i = 30; // (3) + i; // (3) + } + i; // (1) + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(4); + + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + let scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("i"); + const v1 = scope.variables[1]; + + expect(scope.references).to.have.length(3); + expect(scope.references[0].resolved).to.be.equal(v1); + expect(scope.references[1].resolved).to.be.equal(v1); + expect(scope.references[2].resolved).to.be.equal(v1); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("i"); + const v3 = scope.variables[0]; + + expect(scope.references).to.have.length(3); + expect(scope.references[0].resolved).to.be.equal(v3); + expect(scope.references[1].resolved).to.be.equal(v3); + expect(scope.references[2].resolved).to.be.equal(v3); + + scope = scopeManager.scopes[3]; + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("i"); + const v2 = scope.variables[0]; + + expect(scope.references).to.have.length(3); + expect(scope.references[0].resolved).to.be.equal(v2); + expect(scope.references[1].resolved).to.be.equal(v2); + expect(scope.references[2].resolved).to.be.equal(v2); + }); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-catch.js b/packages/eslint-scope/tests/es6-catch.js new file mode 100644 index 00000000..5997b66a --- /dev/null +++ b/packages/eslint-scope/tests/es6-catch.js @@ -0,0 +1,90 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 catch", () => { + it("takes binding pattern", () => { + const ast = espree(` + try { + } catch ({ a, b, c, d }) { + let e = 20; + a; + b; + c; + d; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(4); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("block"); + expect(scope.block.type).to.be.equal("BlockStatement"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("catch"); + expect(scope.block.type).to.be.equal("CatchClause"); + expect(scope.isStrict).to.be.false; + + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.be.equal("a"); + expect(scope.variables[1].name).to.be.equal("b"); + expect(scope.variables[2].name).to.be.equal("c"); + expect(scope.variables[3].name).to.be.equal("d"); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[3]; + expect(scope.type).to.be.equal("block"); + expect(scope.block.type).to.be.equal("BlockStatement"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + expect(scope.variables.map(variable => variable.name)).to.be.eql([ + "e" + ]); + expect(scope.references.map(ref => ref.identifier.name)).to.be.eql([ + "e", + "a", + "b", + "c", + "d" + ]); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-class.js b/packages/eslint-scope/tests/es6-class.js new file mode 100644 index 00000000..1d38cb23 --- /dev/null +++ b/packages/eslint-scope/tests/es6-class.js @@ -0,0 +1,394 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 class", () => { + it("declaration name creates class scope", () => { + const ast = espree(` + class Derived extends Base { + constructor() { + } + } + new Derived(); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("Derived"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("Derived"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassDeclaration"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("Derived"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("Base"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); + }); + + it("expression name creates class scope#1", () => { + const ast = espree(` + (class Derived extends Base { + constructor() { + } + }); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("Derived"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("Base"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + }); + + it("expression name creates class scope#2", () => { + const ast = espree(` + (class extends Base { + constructor() { + } + }); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("Base"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + }); + + it("computed property key may refer variables", () => { + const ast = espree(` + (function () { + var yuyushiki = 42; + (class { + [yuyushiki]() { + } + + [yuyushiki + 40]() { + } + }); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(5); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("yuyushiki"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("yuyushiki"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("yuyushiki"); + expect(scope.references[1].identifier.name).to.be.equal("yuyushiki"); + }); + + // https://github.com/eslint/eslint-scope/issues/59 + it("class heritage may refer class name in class expressions #1", () => { + const ast = espree(` + const A = class A extends A {} + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("A"); // variable `A` defined by `const A` + expect(scope.variables[0].references).to.have.length(1); // init reference `A` in `const A` + expect(scope.variables[0].references[0].init).to.be.true; + expect(scope.references).to.have.length(1); + expect(scope.references[0]).to.be.equal(scope.variables[0].references[0]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("A"); // variable `A` defined by `class A` + expect(scope.variables[0].references).to.have.length(1); // reference `A` in `extends A` + expect(scope.references).to.have.length(1); + expect(scope.references[0].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[0]).to.be.equal(scope.variables[0].references[0]); + }); + + it("class heritage may refer class name in class expressions #2", () => { + const ast = espree(` + let foo; + (class C extends (foo = C, class {}) {}); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("foo"); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("C"); + expect(scope.variables[0].references).to.have.length(1); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("foo"); + expect(scope.references[1].identifier.name).to.be.equal("C"); + + // `C` in `foo = C` is a reference to variable `C` defined by `class C` + expect(scope.references[1].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[1]).to.be.equal(scope.variables[0].references[0]); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + }); + + it("class heritage may refer class name in class declarations", () => { + const ast = espree(` + let foo; + class C extends (foo = C, class {}) {} + new C(); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("foo"); + expect(scope.variables[0].references).to.have.length(1); + expect(scope.variables[1].name).to.be.equal("C"); + expect(scope.variables[1].references).to.have.length(1); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("C"); // `C` in `new C()` + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[0]).to.be.equal(scope.variables[1].references[0]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassDeclaration"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("C"); + expect(scope.variables[0].references).to.have.length(1); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("foo"); + expect(scope.references[1].identifier.name).to.be.equal("C"); // `C` in `foo = C` + + /* + * `class C` creates two variables `C`: one in the scope where the class + * is declared, another in the class scope. References inside the class + * should be to the variable in the class scope. + */ + expect(scope.references[1].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[1]).to.be.equal(scope.variables[0].references[0]); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + }); + + it("inner scopes in the class heritage of a class expression are nested in the class scope", () => { + const ast = espree(` + (class extends function () {} {}) + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassExpression"); + expect(scope.isStrict).to.be.true; + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.upper).to.be.equal(scopeManager.scopes[1]); + expect(scopeManager.scopes[1].childScopes).to.have.length(1); + expect(scopeManager.scopes[1].childScopes[0]).to.be.equal(scope); + }); + + it("inner scopes in the class heritage of a class declaration are nested in the class scope", () => { + const ast = espree(` + class C extends function () {} {} + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.block.type).to.be.equal("ClassDeclaration"); + expect(scope.isStrict).to.be.true; + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.upper).to.be.equal(scopeManager.scopes[1]); + expect(scopeManager.scopes[1].childScopes).to.have.length(1); + expect(scopeManager.scopes[1].childScopes[0]).to.be.equal(scope); + }); + + it("regression #49", () => { + const ast = espree(` + class Shoe { + constructor() { + //Shoe.x = true; + } + } + let shoe = new Shoe(); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + const scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("Shoe"); + expect(scope.variables[1].name).to.be.equal("shoe"); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("shoe"); + expect(scope.references[1].identifier.name).to.be.equal("Shoe"); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-default-parameters.js b/packages/eslint-scope/tests/es6-default-parameters.js new file mode 100644 index 00000000..3c957f67 --- /dev/null +++ b/packages/eslint-scope/tests/es6-default-parameters.js @@ -0,0 +1,354 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Toru Nagashima +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 default parameters:", () => { + describe("a default parameter creates a writable reference for its initialization:", () => { + const patterns = { + FunctionDeclaration: "function foo(a, b = 0) {}", + FunctionExpression: "let foo = function(a, b = 0) {};", + ArrowExpression: "let foo = (a, b = 0) => {};" + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const numVars = name === "ArrowExpression" ? 2 : 3; + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(numVars); // [arguments?, a, b] + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("b"); + expect(reference.resolved).to.equal(scope.variables[numVars - 1]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + } + }); + + describe("a default parameter creates a readable reference for references in right:", () => { + const patterns = { + FunctionDeclaration: ` + let a; + function foo(b = a) {} + `, + FunctionExpression: ` + let a; + let foo = function(b = a) {} + `, + ArrowExpression: ` + let a; + let foo = (b = a) => {}; + ` + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const numVars = name === "ArrowExpression" ? 1 : 2; + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(numVars); // [arguments?, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + } + }); + + describe("a default parameter creates a readable reference for references in right (for const):", () => { + const patterns = { + FunctionDeclaration: ` + const a = 0; + function foo(b = a) {} + `, + FunctionExpression: ` + const a = 0; + let foo = function(b = a) {} + `, + ArrowExpression: ` + const a = 0; + let foo = (b = a) => {}; + ` + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const numVars = name === "ArrowExpression" ? 1 : 2; + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(numVars); // [arguments?, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + } + }); + + describe("a default parameter creates a readable reference for references in right (partial):", () => { + const patterns = { + FunctionDeclaration: ` + let a; + function foo(b = a.c) {} + `, + FunctionExpression: ` + let a; + let foo = function(b = a.c) {} + `, + ArrowExpression: ` + let a; + let foo = (b = a.c) => {}; + ` + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const numVars = name === "ArrowExpression" ? 1 : 2; + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(numVars); // [arguments?, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + } + }); + + describe("a default parameter creates a readable reference for references in right's nested scope:", () => { + const patterns = { + FunctionDeclaration: ` + let a; + function foo(b = function() { return a; }) {} + `, + FunctionExpression: ` + let a; + let foo = function(b = function() { return a; }) {} + `, + ArrowExpression: ` + let a; + let foo = (b = function() { return a; }) => {}; + ` + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, foo, anonymous] + + const scope = scopeManager.scopes[2]; + + expect(scope.variables).to.have.length(1); // [arguments] + expect(scope.references).to.have.length(1); // [a] + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + } + }); + + describe("a default parameter creates a readable reference for references in right. It's resolved to outer scope's even if there is the variable in the function body:", () => { + const patterns = { + FunctionDeclaration: ` + let a; + function foo(b = a) { let a; } + `, + FunctionExpression: ` + let a; + let foo = function(b = a) { let a; } + `, + ArrowExpression: ` + let a; + let foo = (b = a) => { let a; }; + ` + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const numVars = name === "ArrowExpression" ? 2 : 3; + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(numVars); // [arguments?, b, a] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + } + }); + + describe("a default parameter creates a readable reference for references in right. It's resolved to the parameter:", () => { + const patterns = { + FunctionDeclaration: ` + let a; + function foo(b = a, a) { } + `, + FunctionExpression: ` + let a; + let foo = function(b = a, a) { } + `, + ArrowExpression: ` + let a; + let foo = (b = a, a) => { }; + ` + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const numVars = name === "ArrowExpression" ? 2 : 3; + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(numVars); // [arguments?, b, a] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables.at(-1)); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + } + }); + + describe("a default parameter creates a readable reference for references in right (nested scope). It's resolved to outer scope's even if there is the variable in the function body:", () => { + const patterns = { + FunctionDeclaration: ` + let a; + function foo(b = function(){ a }) { let a; } + `, + FunctionExpression: ` + let a; + let foo = function(b = function(){ a }) { let a; } + `, + ArrowExpression: ` + let a; + let foo = (b = function(){ a }) => { let a; }; + ` + }; + + for (const [name, code] of Object.entries(patterns)) { + it(name, () => { + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, foo, anonymous function] + + const scope = scopeManager.scopes[2]; + + expect(scope.references).to.have.length(1); // [a] + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + } + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-destructuring-assignments.js b/packages/eslint-scope/tests/es6-destructuring-assignments.js new file mode 100644 index 00000000..c9ca8038 --- /dev/null +++ b/packages/eslint-scope/tests/es6-destructuring-assignments.js @@ -0,0 +1,1324 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 destructuring assignments", () => { + it("Pattern in var in ForInStatement", () => { + const ast = espree(` + (function () { + for (var [a, b, c] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("a"); + expect(scope.variables[2].name).to.be.equal("b"); + expect(scope.variables[3].name).to.be.equal("c"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.be.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.be.equal(scope.variables[2]); + expect(scope.references[2].identifier.name).to.be.equal("c"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.be.equal(scope.variables[3]); + expect(scope.references[3].identifier.name).to.be.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + }); + + it("Pattern in let in ForInStatement", () => { + const ast = espree(` + (function () { + for (let [a, b, c] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, function, for] + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.equal("array"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.equal("for"); + expect(scope.variables).to.have.length(3); + expect(scope.variables[0].name).to.equal("a"); + expect(scope.variables[1].name).to.equal("b"); + expect(scope.variables[2].name).to.equal("c"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.equal(scope.variables[0]); + expect(scope.references[1].identifier.name).to.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.equal(scope.variables[1]); + expect(scope.references[2].identifier.name).to.equal("c"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.equal(scope.variables[2]); + expect(scope.references[3].identifier.name).to.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + expect(scope.references[3].resolved).to.be.null; + }); + + it("Pattern with default values in var in ForInStatement", () => { + const ast = espree(` + (function () { + for (var [a, b, c = d] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(2); + expect(scope.implicit.left[0].identifier.name).to.equal("d"); + expect(scope.implicit.left[1].identifier.name).to.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.equal("arguments"); + expect(scope.variables[1].name).to.equal("a"); + expect(scope.variables[2].name).to.equal("b"); + expect(scope.variables[3].name).to.equal("c"); + expect(scope.references).to.have.length(6); + expect(scope.references[0].identifier.name).to.equal("c"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].writeExpr.name).to.equal("d"); + expect(scope.references[0].partial).to.be.false; + expect(scope.references[0].resolved).to.equal(scope.variables[3]); + expect(scope.references[1].identifier.name).to.equal("d"); + expect(scope.references[1].isWrite()).to.be.false; + expect(scope.references[2].identifier.name).to.equal("a"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.equal(scope.variables[1]); + expect(scope.references[3].identifier.name).to.equal("b"); + expect(scope.references[3].isWrite()).to.be.true; + expect(scope.references[3].partial).to.be.true; + expect(scope.references[3].resolved).to.equal(scope.variables[2]); + expect(scope.references[4].identifier.name).to.equal("c"); + expect(scope.references[4].isWrite()).to.be.true; + expect(scope.references[4].writeExpr.name).to.equal("array"); + expect(scope.references[4].partial).to.be.true; + expect(scope.references[4].resolved).to.equal(scope.variables[3]); + expect(scope.references[5].identifier.name).to.equal("array"); + expect(scope.references[5].isWrite()).to.be.false; + }); + + it("Pattern with default values in let in ForInStatement", () => { + const ast = espree(` + (function () { + for (let [a, b, c = d] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, function, for] + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(2); + expect(scope.implicit.left[0].identifier.name).to.equal("d"); + expect(scope.implicit.left[0].from.type).to.equal("for"); + expect(scope.implicit.left[1].identifier.name).to.equal("array"); + expect(scope.implicit.left[1].from.type).to.equal("for"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.equal("for"); + expect(scope.variables).to.have.length(3); + expect(scope.variables[0].name).to.equal("a"); + expect(scope.variables[1].name).to.equal("b"); + expect(scope.variables[2].name).to.equal("c"); + expect(scope.references).to.have.length(6); + expect(scope.references[0].identifier.name).to.equal("c"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].writeExpr.name).to.equal("d"); + expect(scope.references[0].partial).to.be.false; + expect(scope.references[0].resolved).to.equal(scope.variables[2]); + expect(scope.references[1].identifier.name).to.equal("d"); + expect(scope.references[1].isWrite()).to.be.false; + expect(scope.references[2].identifier.name).to.equal("a"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].writeExpr.name).to.equal("array"); + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.equal(scope.variables[0]); + expect(scope.references[3].identifier.name).to.equal("b"); + expect(scope.references[3].isWrite()).to.be.true; + expect(scope.references[3].writeExpr.name).to.equal("array"); + expect(scope.references[3].partial).to.be.true; + expect(scope.references[3].resolved).to.equal(scope.variables[1]); + expect(scope.references[4].identifier.name).to.equal("c"); + expect(scope.references[4].isWrite()).to.be.true; + expect(scope.references[4].writeExpr.name).to.equal("array"); + expect(scope.references[4].partial).to.be.true; + expect(scope.references[4].resolved).to.equal(scope.variables[2]); + expect(scope.references[5].identifier.name).to.equal("array"); + expect(scope.references[5].isWrite()).to.be.false; + expect(scope.references[5].resolved).to.be.null; + }); + + it("Pattern with nested default values in var in ForInStatement", () => { + const ast = espree(` + (function () { + for (var [a, [b, c = d] = e] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(3); + expect(scope.implicit.left[0].identifier.name).to.equal("d"); + expect(scope.implicit.left[1].identifier.name).to.equal("e"); + expect(scope.implicit.left[2].identifier.name).to.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.equal("arguments"); + expect(scope.variables[1].name).to.equal("a"); + expect(scope.variables[2].name).to.equal("b"); + expect(scope.variables[3].name).to.equal("c"); + expect(scope.references).to.have.length(9); + expect(scope.references[0].identifier.name).to.equal("b"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].writeExpr.name).to.equal("e"); + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.equal(scope.variables[2]); + expect(scope.references[1].identifier.name).to.equal("c"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].writeExpr.name).to.equal("e"); + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.equal(scope.variables[3]); + expect(scope.references[2].identifier.name).to.equal("c"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].writeExpr.name).to.equal("d"); + expect(scope.references[2].partial).to.be.false; + expect(scope.references[2].resolved).to.equal(scope.variables[3]); + expect(scope.references[3].identifier.name).to.equal("d"); + expect(scope.references[3].isWrite()).to.be.false; + expect(scope.references[4].identifier.name).to.equal("e"); + expect(scope.references[4].isWrite()).to.be.false; + expect(scope.references[5].identifier.name).to.equal("a"); + expect(scope.references[5].isWrite()).to.be.true; + expect(scope.references[5].writeExpr.name).to.equal("array"); + expect(scope.references[5].partial).to.be.true; + expect(scope.references[5].resolved).to.equal(scope.variables[1]); + expect(scope.references[6].identifier.name).to.equal("b"); + expect(scope.references[6].isWrite()).to.be.true; + expect(scope.references[6].writeExpr.name).to.equal("array"); + expect(scope.references[6].partial).to.be.true; + expect(scope.references[6].resolved).to.equal(scope.variables[2]); + expect(scope.references[7].identifier.name).to.equal("c"); + expect(scope.references[7].isWrite()).to.be.true; + expect(scope.references[7].writeExpr.name).to.equal("array"); + expect(scope.references[7].partial).to.be.true; + expect(scope.references[7].resolved).to.equal(scope.variables[3]); + expect(scope.references[8].identifier.name).to.equal("array"); + expect(scope.references[8].isWrite()).to.be.false; + }); + + it("Pattern with nested default values in let in ForInStatement", () => { + const ast = espree(` + (function () { + for (let [a, [b, c = d] = e] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, function, for] + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(3); + expect(scope.implicit.left[0].identifier.name).to.equal("d"); + expect(scope.implicit.left[0].from.type).to.equal("for"); + expect(scope.implicit.left[1].identifier.name).to.equal("e"); + expect(scope.implicit.left[1].from.type).to.equal("for"); + expect(scope.implicit.left[2].identifier.name).to.equal("array"); + expect(scope.implicit.left[2].from.type).to.equal("for"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.equal("for"); + expect(scope.variables).to.have.length(3); + expect(scope.variables[0].name).to.equal("a"); + expect(scope.variables[1].name).to.equal("b"); + expect(scope.variables[2].name).to.equal("c"); + expect(scope.references).to.have.length(9); + expect(scope.references[0].identifier.name).to.equal("b"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].writeExpr.name).to.equal("e"); + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.equal("c"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].writeExpr.name).to.equal("e"); + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.equal(scope.variables[2]); + expect(scope.references[2].identifier.name).to.equal("c"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].writeExpr.name).to.equal("d"); + expect(scope.references[2].partial).to.be.false; + expect(scope.references[2].resolved).to.equal(scope.variables[2]); + expect(scope.references[3].identifier.name).to.equal("d"); + expect(scope.references[3].isWrite()).to.be.false; + expect(scope.references[4].identifier.name).to.equal("e"); + expect(scope.references[4].isWrite()).to.be.false; + expect(scope.references[5].identifier.name).to.equal("a"); + expect(scope.references[5].isWrite()).to.be.true; + expect(scope.references[5].writeExpr.name).to.equal("array"); + expect(scope.references[5].partial).to.be.true; + expect(scope.references[5].resolved).to.equal(scope.variables[0]); + expect(scope.references[6].identifier.name).to.equal("b"); + expect(scope.references[6].isWrite()).to.be.true; + expect(scope.references[6].writeExpr.name).to.equal("array"); + expect(scope.references[6].partial).to.be.true; + expect(scope.references[6].resolved).to.equal(scope.variables[1]); + expect(scope.references[7].identifier.name).to.equal("c"); + expect(scope.references[7].isWrite()).to.be.true; + expect(scope.references[7].writeExpr.name).to.equal("array"); + expect(scope.references[7].partial).to.be.true; + expect(scope.references[7].resolved).to.equal(scope.variables[2]); + expect(scope.references[8].identifier.name).to.equal("array"); + expect(scope.references[8].isWrite()).to.be.false; + expect(scope.references[8].resolved).to.be.null; + }); + + it("Pattern with default values in var in ForInStatement (separate declarations)", () => { + const ast = espree(` + (function () { + var a, b, c; + for ([a, b, c = d] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(2); + expect(scope.implicit.left[0].identifier.name).to.equal("d"); + expect(scope.implicit.left[1].identifier.name).to.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.equal("arguments"); + expect(scope.variables[1].name).to.equal("a"); + expect(scope.variables[2].name).to.equal("b"); + expect(scope.variables[3].name).to.equal("c"); + expect(scope.references).to.have.length(6); + expect(scope.references[0].identifier.name).to.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.equal(scope.variables[2]); + expect(scope.references[2].identifier.name).to.equal("c"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].writeExpr.name).to.equal("d"); + expect(scope.references[2].partial).to.be.false; + expect(scope.references[2].resolved).to.equal(scope.variables[3]); + expect(scope.references[3].identifier.name).to.equal("c"); + expect(scope.references[3].isWrite()).to.be.true; + expect(scope.references[3].writeExpr.name).to.equal("array"); + expect(scope.references[3].partial).to.be.true; + expect(scope.references[3].resolved).to.equal(scope.variables[3]); + expect(scope.references[4].identifier.name).to.equal("d"); + expect(scope.references[4].isWrite()).to.be.false; + expect(scope.references[5].identifier.name).to.equal("array"); + expect(scope.references[5].isWrite()).to.be.false; + }); + + it("Pattern with default values in var in ForInStatement (separate declarations and with MemberExpression)", () => { + const ast = espree(` + (function () { + var obj; + for ([obj.a, obj.b, obj.c = d] in array); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(2); + expect(scope.implicit.left[0].identifier.name).to.equal("d"); + expect(scope.implicit.left[1].identifier.name).to.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.equal("function"); + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.equal("arguments"); + expect(scope.variables[1].name).to.equal("obj"); + expect(scope.references).to.have.length(5); + expect(scope.references[0].identifier.name).to.equal("obj"); // obj.a + expect(scope.references[0].isWrite()).to.be.false; + expect(scope.references[0].isRead()).to.be.true; + expect(scope.references[0].resolved).to.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.equal("obj"); // obj.b + expect(scope.references[1].isWrite()).to.be.false; + expect(scope.references[1].isRead()).to.be.true; + expect(scope.references[1].resolved).to.equal(scope.variables[1]); + expect(scope.references[2].identifier.name).to.equal("obj"); // obj.c + expect(scope.references[2].isWrite()).to.be.false; + expect(scope.references[2].isRead()).to.be.true; + expect(scope.references[2].resolved).to.equal(scope.variables[1]); + expect(scope.references[3].identifier.name).to.equal("d"); + expect(scope.references[3].isWrite()).to.be.false; + expect(scope.references[3].isRead()).to.be.true; + expect(scope.references[4].identifier.name).to.equal("array"); + expect(scope.references[4].isWrite()).to.be.false; + expect(scope.references[4].isRead()).to.be.true; + }); + + it("ArrayPattern in var", () => { + const ast = espree(` + (function () { + var [a, b, c] = array; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("a"); + expect(scope.variables[2].name).to.be.equal("b"); + expect(scope.variables[3].name).to.be.equal("c"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.be.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.be.equal(scope.variables[2]); + expect(scope.references[2].identifier.name).to.be.equal("c"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.be.equal(scope.variables[3]); + expect(scope.references[3].identifier.name).to.be.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + }); + + it("SpreadElement in var", () => { + let ast = espree(` + (function () { + var [a, b, ...rest] = array; + }()); + `); + + let scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("a"); + expect(scope.variables[2].name).to.be.equal("b"); + expect(scope.variables[3].name).to.be.equal("rest"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.be.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.be.equal(scope.variables[2]); + expect(scope.references[2].identifier.name).to.be.equal("rest"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.be.equal(scope.variables[3]); + expect(scope.references[3].identifier.name).to.be.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + + ast = espree(` + (function () { + var [a, b, ...[c, d, ...rest]] = array; + }()); + `); + + scopeManager = analyze(ast, { ecmaVersion: 6 }); + expect(scopeManager.scopes).to.have.length(2); + + scope = scopeManager.scopes[0]; + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + + expect(scope.variables).to.have.length(6); + const expectedVariableNames = [ + "arguments", + "a", + "b", + "c", + "d", + "rest" + ]; + + for (let index = 0; index < expectedVariableNames.length; index++) { + expect(scope.variables[index].name).to.be.equal(expectedVariableNames[index]); + } + + expect(scope.references).to.have.length(6); + const expectedReferenceNames = [ + "a", + "b", + "c", + "d", + "rest" + ]; + + for (let index = 0; index < expectedReferenceNames.length; index++) { + expect(scope.references[index].identifier.name).to.be.equal(expectedReferenceNames[index]); + expect(scope.references[index].isWrite()).to.be.true; + expect(scope.references[index].partial).to.be.true; + } + expect(scope.references[5].identifier.name).to.be.equal("array"); + expect(scope.references[5].isWrite()).to.be.false; + }); + + it("ObjectPattern in var", () => { + const ast = espree(` + (function () { + var { + shorthand, + key: value, + hello: { + world + } + } = object; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("object"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("shorthand"); + expect(scope.variables[2].name).to.be.equal("value"); + expect(scope.variables[3].name).to.be.equal("world"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("shorthand"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.be.equal("value"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.be.equal(scope.variables[2]); + expect(scope.references[2].identifier.name).to.be.equal("world"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.be.equal(scope.variables[3]); + expect(scope.references[3].identifier.name).to.be.equal("object"); + expect(scope.references[3].isWrite()).to.be.false; + }); + + it("complex pattern in var", () => { + const ast = espree(` + (function () { + var { + shorthand, + key: [ a, b, c, d, e ], + hello: { + world + } + } = object; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("object"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(8); + const expectedVariableNames = [ + "arguments", + "shorthand", + "a", + "b", + "c", + "d", + "e", + "world" + ]; + + for (let index = 0; index < expectedVariableNames.length; index++) { + expect(scope.variables[index].name).to.be.equal(expectedVariableNames[index]); + } + expect(scope.references).to.have.length(8); + const expectedReferenceNames = [ + "shorthand", + "a", + "b", + "c", + "d", + "e", + "world" + ]; + + for (let index = 0; index < expectedReferenceNames.length; index++) { + expect(scope.references[index].identifier.name).to.be.equal(expectedReferenceNames[index]); + expect(scope.references[index].isWrite()).to.be.true; + expect(scope.references[index].partial).to.be.true; + } + expect(scope.references[7].identifier.name).to.be.equal("object"); + expect(scope.references[7].isWrite()).to.be.false; + }); + + it("ArrayPattern in AssignmentExpression", () => { + const ast = espree(` + (function () { + [a, b, c] = array; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(4); + expect(scope.implicit.left.map(left => left.identifier.name)).to.deep.equal([ + "a", + "b", + "c", + "array" + ]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.be.null; + expect(scope.references[1].identifier.name).to.be.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.be.null; + expect(scope.references[2].identifier.name).to.be.equal("c"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.be.null; + expect(scope.references[3].identifier.name).to.be.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + }); + + it("ArrayPattern with MemberExpression in AssignmentExpression", () => { + const ast = espree(` + (function () { + var obj; + [obj.a, obj.b, obj.c] = array; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.equal("function"); + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.equal("arguments"); + expect(scope.variables[1].name).to.equal("obj"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.equal("obj"); + expect(scope.references[0].isWrite()).to.be.false; + expect(scope.references[0].isRead()).to.be.true; + expect(scope.references[0].resolved).to.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.equal("obj"); + expect(scope.references[1].isWrite()).to.be.false; + expect(scope.references[1].isRead()).to.be.true; + expect(scope.references[1].resolved).to.equal(scope.variables[1]); + expect(scope.references[2].identifier.name).to.equal("obj"); + expect(scope.references[2].isWrite()).to.be.false; + expect(scope.references[2].isRead()).to.be.true; + expect(scope.references[2].resolved).to.equal(scope.variables[1]); + expect(scope.references[3].identifier.name).to.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + expect(scope.references[3].isRead()).to.be.true; + }); + + it("SpreadElement in AssignmentExpression", () => { + let ast = espree(` + (function () { + [a, b, ...rest] = array; + }()); + `); + + let scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(4); + expect(scope.implicit.left.map(left => left.identifier.name)).to.deep.equal([ + "a", + "b", + "rest", + "array" + ]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.be.null; + expect(scope.references[1].identifier.name).to.be.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.be.null; + expect(scope.references[2].identifier.name).to.be.equal("rest"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.be.null; + expect(scope.references[3].identifier.name).to.be.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + + ast = espree(` + (function () { + [a, b, ...[c, d, ...rest]] = array; + }()); + `); + + scopeManager = analyze(ast, { ecmaVersion: 6 }); + expect(scopeManager.scopes).to.have.length(2); + + scope = scopeManager.scopes[0]; + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(6); + expect(scope.implicit.left.map(left => left.identifier.name)).to.deep.equal([ + "a", + "b", + "c", + "d", + "rest", + "array" + ]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + + expect(scope.references).to.have.length(6); + const expectedReferenceNames = [ + "a", + "b", + "c", + "d", + "rest" + ]; + + for (let index = 0; index < expectedReferenceNames.length; index++) { + expect(scope.references[index].identifier.name).to.be.equal(expectedReferenceNames[index]); + expect(scope.references[index].isWrite()).to.be.true; + expect(scope.references[index].partial).to.be.true; + expect(scope.references[index].resolved).to.be.null; + } + expect(scope.references[5].identifier.name).to.be.equal("array"); + expect(scope.references[5].isWrite()).to.be.false; + }); + + it("SpreadElement with MemberExpression in AssignmentExpression", () => { + const ast = espree(` + (function () { + [a, b, ...obj.rest] = array; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(4); + expect(scope.implicit.left.map(left => left.identifier.name)).to.deep.equal([ + "a", + "b", + "obj", + "array" + ]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.equal("arguments"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.equal("a"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.be.null; + expect(scope.references[1].identifier.name).to.equal("b"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.be.null; + expect(scope.references[2].identifier.name).to.equal("obj"); + expect(scope.references[2].isWrite()).to.be.false; + expect(scope.references[3].identifier.name).to.equal("array"); + expect(scope.references[3].isWrite()).to.be.false; + }); + + it("ObjectPattern in AssignmentExpression", () => { + const ast = espree(` + (function () { + ({ + shorthand, + key: value, + hello: { + world + } + } = object); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(4); + expect(scope.implicit.left.map(left => left.identifier.name)).to.deep.equal([ + "shorthand", + "value", + "world", + "object" + ]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("shorthand"); + expect(scope.references[0].isWrite()).to.be.true; + expect(scope.references[0].partial).to.be.true; + expect(scope.references[0].resolved).to.null; + expect(scope.references[1].identifier.name).to.be.equal("value"); + expect(scope.references[1].isWrite()).to.be.true; + expect(scope.references[1].partial).to.be.true; + expect(scope.references[1].resolved).to.null; + expect(scope.references[2].identifier.name).to.be.equal("world"); + expect(scope.references[2].isWrite()).to.be.true; + expect(scope.references[2].partial).to.be.true; + expect(scope.references[2].resolved).to.null; + expect(scope.references[3].identifier.name).to.be.equal("object"); + expect(scope.references[3].isWrite()).to.be.false; + }); + + it("complex pattern in AssignmentExpression", () => { + const ast = espree(` + (function () { + ({ + shorthand, + key: [ a, b, c, d, e ], + hello: { + world + } + } = object); + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + expect(scope.implicit.left).to.have.length(8); + expect(scope.implicit.left.map(left => left.identifier.name)).to.deep.equal([ + "shorthand", + "a", + "b", + "c", + "d", + "e", + "world", + "object" + ]); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(8); + const expectedReferenceNames = [ + "shorthand", + "a", + "b", + "c", + "d", + "e", + "world" + ]; + + for (let index = 0; index < expectedReferenceNames.length; index++) { + expect(scope.references[index].identifier.name).to.be.equal(expectedReferenceNames[index]); + expect(scope.references[index].isWrite()).to.be.true; + expect(scope.references[index].partial).to.be.true; + } + expect(scope.references[7].identifier.name).to.be.equal("object"); + expect(scope.references[7].isWrite()).to.be.false; + }); + + it("ArrayPattern in parameters", () => { + const ast = espree(` + (function ([a, b, c]) { + }(array)); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("array"); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("a"); + expect(scope.variables[2].name).to.be.equal("b"); + expect(scope.variables[3].name).to.be.equal("c"); + expect(scope.references).to.have.length(0); + }); + + it("SpreadElement in parameters", () => { + const ast = espree(` + (function ([a, b, ...rest], ...rest2) { + }(array)); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("array"); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("array"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(5); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("a"); + expect(scope.variables[2].name).to.be.equal("b"); + expect(scope.variables[3].name).to.be.equal("rest"); + expect(scope.variables[3].defs[0].rest).to.be.true; + expect(scope.variables[4].name).to.be.equal("rest2"); + expect(scope.variables[4].defs[0].rest).to.be.true; + expect(scope.references).to.have.length(0); + }); + + it("ObjectPattern in parameters", () => { + const ast = espree(` + (function ({ + shorthand, + key: value, + hello: { + world + } + }) { + }(object)); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("object"); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("object"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(4); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("shorthand"); + expect(scope.variables[2].name).to.be.equal("value"); + expect(scope.variables[3].name).to.be.equal("world"); + expect(scope.references).to.have.length(0); + }); + + it("complex pattern in parameters", () => { + const ast = espree(` + (function ({ + shorthand, + key: [ a, b, c, d, e ], + hello: { + world + } + }) { + }(object)); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("object"); + expect(scope.implicit.left).to.have.length(1); + expect(scope.implicit.left[0].identifier.name).to.be.equal("object"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(8); + const expectedVariableNames = [ + "arguments", + "shorthand", + "a", + "b", + "c", + "d", + "e", + "world" + ]; + + for (let index = 0; index < expectedVariableNames.length; index++) { + expect(scope.variables[index].name).to.be.equal(expectedVariableNames[index]); + } + expect(scope.references).to.have.length(0); + }); + + it("default values and patterns in var", () => { + const ast = espree(` + (function () { + var [a, b, c, d = 20 ] = array; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(5); + const expectedVariableNames = [ + "arguments", + "a", + "b", + "c", + "d" + ]; + + for (let index = 0; index < expectedVariableNames.length; index++) { + expect(scope.variables[index].name).to.be.equal(expectedVariableNames[index]); + } + expect(scope.references).to.have.length(6); + const expectedReferenceNames = [ + "a", + "b", + "c", + "d", // assign 20 + "d", // assign array + "array" + ]; + + for (let index = 0; index < expectedReferenceNames.length; index++) { + expect(scope.references[index].identifier.name).to.be.equal(expectedReferenceNames[index]); + } + }); + + it("default values containing references and patterns in var", () => { + const ast = espree(` + (function () { + var [a, b, c, d = e ] = array; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(5); + const expectedVariableNames = [ + "arguments", + "a", + "b", + "c", + "d" + ]; + + for (let index = 0; index < expectedVariableNames.length; index++) { + expect(scope.variables[index].name).to.be.equal(expectedVariableNames[index]); + } + expect(scope.references).to.have.length(7); + const expectedReferenceNames = [ + "a", // assign array + "b", // assign array + "c", // assign array + "d", // assign e + "d", // assign array + "e", + "array" + ]; + + for (let index = 0; index < expectedReferenceNames.length; index++) { + expect(scope.references[index].identifier.name).to.be.equal(expectedReferenceNames[index]); + } + }); + + it("nested default values containing references and patterns in var", () => { + const ast = espree(` + (function () { + var [a, b, [c, d = e] = f ] = array; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.equal("global"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.equal("function"); + expect(scope.variables).to.have.length(5); + const expectedVariableNames = [ + "arguments", + "a", + "b", + "c", + "d" + ]; + + for (let index = 0; index < expectedVariableNames.length; index++) { + expect(scope.variables[index].name).to.equal(expectedVariableNames[index]); + } + expect(scope.references).to.have.length(10); + const expectedReferenceNames = [ + "a", // assign array + "b", // assign array + "c", // assign f + "c", // assign array + "d", // assign f + "d", // assign e + "d", // assign array + "e", + "f", + "array" + ]; + + for (let index = 0; index < expectedReferenceNames.length; index++) { + expect(scope.references[index].identifier.name).to.equal(expectedReferenceNames[index]); + } + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-export.js b/packages/eslint-scope/tests/es6-export.js new file mode 100644 index 00000000..b683f62c --- /dev/null +++ b/packages/eslint-scope/tests/es6-export.js @@ -0,0 +1,223 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("export declaration", () => { + + // http://people.mozilla.org/~jorendorff/es6-draft.html#sec-static-and-runtme-semantics-module-records + it("should create variable bindings", () => { + const ast = espree("export var v;"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("v"); + expect(scope.variables[0].defs[0].type).to.be.equal("Variable"); + expect(scope.references).to.have.length(0); + }); + + it("should create function declaration bindings", () => { + const ast = espree("export default function f(){};"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(3); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + let scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("f"); + expect(scope.variables[0].defs[0].type).to.be.equal("FunctionName"); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); + }); + + it("should export function expression", () => { + const ast = espree("export default function(){};"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(3); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + let scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); + }); + + it("should export literal", () => { + const ast = espree("export default 42;"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + }); + + it("should refer exported references#1", () => { + const ast = espree("const x = 1; export {x};"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("x"); + expect(scope.references[1].identifier.name).to.be.equal("x"); + }); + + it("should refer exported references#2", () => { + const ast = espree("const v = 1; export {v as x};"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("v"); + expect(scope.references[1].identifier.name).to.be.equal("v"); + }); + + it("should not refer exported references from other source#1", () => { + const ast = espree("export {x} from \"mod\";"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + }); + + it("should not refer exported references from other source#2", () => { + const ast = espree("export {v as x} from \"mod\";"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + }); + + it("should not refer exported references from other source#3", () => { + const ast = espree("export * from \"mod\";"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-import.js b/packages/eslint-scope/tests/es6-import.js new file mode 100644 index 00000000..be520768 --- /dev/null +++ b/packages/eslint-scope/tests/es6-import.js @@ -0,0 +1,133 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { getSupportedEcmaVersions } from "./util/ecma-version.js"; +import { analyze } from "../lib/index.js"; + +describe("import declaration", () => { + + // http://people.mozilla.org/~jorendorff/es6-draft.html#sec-static-and-runtme-semantics-module-records + it("should import names from source", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree("import v from \"mod\";"); + + const scopeManager = analyze(ast, { ecmaVersion, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("v"); + expect(scope.variables[0].defs[0].type).to.be.equal("ImportBinding"); + expect(scope.references).to.have.length(0); + }); + + }); + + it("should import namespaces", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree("import * as ns from \"mod\";"); + + const scopeManager = analyze(ast, { ecmaVersion, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("ns"); + expect(scope.variables[0].defs[0].type).to.be.equal("ImportBinding"); + expect(scope.references).to.have.length(0); + }); + }); + + it("should import insided names#1", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree("import {x} from \"mod\";"); + + const scopeManager = analyze(ast, { ecmaVersion, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("x"); + expect(scope.variables[0].defs[0].type).to.be.equal("ImportBinding"); + expect(scope.references).to.have.length(0); + }); + }); + + it("should import insided names#2", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree("import {x as v} from \"mod\";"); + + const scopeManager = analyze(ast, { ecmaVersion, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("module"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("v"); + expect(scope.variables[0].defs[0].type).to.be.equal("ImportBinding"); + expect(scope.references).to.have.length(0); + }); + }); + + // TODO: Should parse it. + // import from "mod"; +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-iteration-scope.js b/packages/eslint-scope/tests/es6-iteration-scope.js new file mode 100644 index 00000000..b032ef6b --- /dev/null +++ b/packages/eslint-scope/tests/es6-iteration-scope.js @@ -0,0 +1,214 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { getSupportedEcmaVersions } from "./util/ecma-version.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 iteration scope", () => { + it("let materialize iteration scope for ForInStatement#1", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + (function () { + let i = 20; + for (let i in i) { + console.log(i); + } + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(4); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("i"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("i"); + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + + const iterScope = scope = scopeManager.scopes[2]; + + expect(scope.type).to.be.equal("for"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("i"); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("i"); + expect(scope.references[0].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[1].identifier.name).to.be.equal("i"); + expect(scope.references[1].resolved).to.be.equal(scope.variables[0]); + + scope = scopeManager.scopes[3]; + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("console"); + expect(scope.references[0].resolved).to.be.equal(null); + expect(scope.references[1].identifier.name).to.be.equal("i"); + expect(scope.references[1].resolved).to.be.equal(iterScope.variables[0]); + }); + }); + + it("let materialize iteration scope for ForInStatement#2", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + (function () { + let i = 20; + for (let { i, j, k } in i) { + console.log(i); + } + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(4); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("i"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("i"); + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + + const iterScope = scope = scopeManager.scopes[2]; + + expect(scope.type).to.be.equal("for"); + expect(scope.variables).to.have.length(3); + expect(scope.variables[0].name).to.be.equal("i"); + expect(scope.variables[1].name).to.be.equal("j"); + expect(scope.variables[2].name).to.be.equal("k"); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("i"); + expect(scope.references[0].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[1].identifier.name).to.be.equal("j"); + expect(scope.references[1].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[2].identifier.name).to.be.equal("k"); + expect(scope.references[2].resolved).to.be.equal(scope.variables[2]); + expect(scope.references[3].identifier.name).to.be.equal("i"); + expect(scope.references[3].resolved).to.be.equal(scope.variables[0]); + + scope = scopeManager.scopes[3]; + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("console"); + expect(scope.references[0].resolved).to.be.equal(null); + expect(scope.references[1].identifier.name).to.be.equal("i"); + expect(scope.references[1].resolved).to.be.equal(iterScope.variables[0]); + }); + }); + + it("let materialize iteration scope for ForStatement#2", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + (function () { + let i = 20; + let obj = {}; + for (let { i, j, k } = obj; i < okok; ++i) { + console.log(i, j, k); + } + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(4); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(0); + + const functionScope = scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(3); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("i"); + expect(scope.variables[2].name).to.be.equal("obj"); + expect(scope.references).to.have.length(2); + expect(scope.references[0].identifier.name).to.be.equal("i"); + expect(scope.references[0].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[1].identifier.name).to.be.equal("obj"); + expect(scope.references[1].resolved).to.be.equal(scope.variables[2]); + + const iterScope = scope = scopeManager.scopes[2]; + + expect(scope.type).to.be.equal("for"); + expect(scope.variables).to.have.length(3); + expect(scope.variables[0].name).to.be.equal("i"); + expect(scope.variables[0].defs[0].type).to.be.equal("Variable"); + expect(scope.variables[1].name).to.be.equal("j"); + expect(scope.variables[1].defs[0].type).to.be.equal("Variable"); + expect(scope.variables[2].name).to.be.equal("k"); + expect(scope.variables[2].defs[0].type).to.be.equal("Variable"); + expect(scope.references).to.have.length(7); + expect(scope.references[0].identifier.name).to.be.equal("i"); + expect(scope.references[0].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[1].identifier.name).to.be.equal("j"); + expect(scope.references[1].resolved).to.be.equal(scope.variables[1]); + expect(scope.references[2].identifier.name).to.be.equal("k"); + expect(scope.references[2].resolved).to.be.equal(scope.variables[2]); + expect(scope.references[3].identifier.name).to.be.equal("obj"); + expect(scope.references[3].resolved).to.be.equal(functionScope.variables[2]); + expect(scope.references[4].identifier.name).to.be.equal("i"); + expect(scope.references[4].resolved).to.be.equal(scope.variables[0]); + expect(scope.references[5].identifier.name).to.be.equal("okok"); + expect(scope.references[5].resolved).to.be.null; + expect(scope.references[6].identifier.name).to.be.equal("i"); + expect(scope.references[6].resolved).to.be.equal(scope.variables[0]); + + scope = scopeManager.scopes[3]; + expect(scope.type).to.be.equal("block"); + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(4); + expect(scope.references[0].identifier.name).to.be.equal("console"); + expect(scope.references[0].resolved).to.be.null; + expect(scope.references[1].identifier.name).to.be.equal("i"); + expect(scope.references[1].resolved).to.be.equal(iterScope.variables[0]); + expect(scope.references[2].identifier.name).to.be.equal("j"); + expect(scope.references[2].resolved).to.be.equal(iterScope.variables[1]); + expect(scope.references[3].identifier.name).to.be.equal("k"); + expect(scope.references[3].resolved).to.be.equal(iterScope.variables[2]); + }); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-new-target.js b/packages/eslint-scope/tests/es6-new-target.js new file mode 100644 index 00000000..2ffd9f16 --- /dev/null +++ b/packages/eslint-scope/tests/es6-new-target.js @@ -0,0 +1,53 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 new.target", () => { + it("should not make references of new.target", () => { + const ast = espree(` + class A { + constructor() { + new.target; + } + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + const scope = scopeManager.scopes[2]; + + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-object.js b/packages/eslint-scope/tests/es6-object.js new file mode 100644 index 00000000..cf6df9a7 --- /dev/null +++ b/packages/eslint-scope/tests/es6-object.js @@ -0,0 +1,93 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 object", () => { + it("method definition", () => { + const ast = espree(` + ({ + constructor() { + } + })`); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); + }); + + it("computed property key may refer variables", () => { + const ast = espree(` + (function () { + var yuyushiki = 42; + ({ + [yuyushiki]() { + }, + + [yuyushiki + 40]() { + } + }) + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(4); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("yuyushiki"); + expect(scope.references).to.have.length(3); + expect(scope.references[0].identifier.name).to.be.equal("yuyushiki"); + expect(scope.references[1].identifier.name).to.be.equal("yuyushiki"); + expect(scope.references[2].identifier.name).to.be.equal("yuyushiki"); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-rest-args.js b/packages/eslint-scope/tests/es6-rest-args.js new file mode 100644 index 00000000..0e139572 --- /dev/null +++ b/packages/eslint-scope/tests/es6-rest-args.js @@ -0,0 +1,57 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 rest arguments", () => { + it("materialize rest argument in scope", () => { + const ast = espree(` + function foo(...bar) { + return bar; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("bar"); + expect(scope.variables[1].defs[0].name.name).to.be.equal("bar"); + expect(scope.variables[1].defs[0].rest).to.be.true; + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-super.js b/packages/eslint-scope/tests/es6-super.js new file mode 100644 index 00000000..e4e52b57 --- /dev/null +++ b/packages/eslint-scope/tests/es6-super.js @@ -0,0 +1,74 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 super", () => { + it("is not handled as reference", () => { + const ast = espree(` + class Foo extends Bar { + constructor() { + super(); + } + + method() { + super.method(); + } + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(4); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("Foo"); + expect(scope.references).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("class"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("Foo"); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("Bar"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); // super is specially handled like `this`. + + scope = scopeManager.scopes[3]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.references).to.have.length(0); // super is specially handled like `this`. + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-switch.js b/packages/eslint-scope/tests/es6-switch.js new file mode 100644 index 00000000..a8f4d3ff --- /dev/null +++ b/packages/eslint-scope/tests/es6-switch.js @@ -0,0 +1,75 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { getSupportedEcmaVersions } from "./util/ecma-version.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 switch", () => { + it("materialize scope", () => { + getSupportedEcmaVersions({ min: 6 }).forEach(ecmaVersion => { + const ast = espree(` + switch (ok) { + case hello: + let i = 20; + i; + break; + + default: + let test = 30; + test; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(1); + expect(scope.references[0].identifier.name).to.be.equal("ok"); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("switch"); + expect(scope.block.type).to.be.equal("SwitchStatement"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("i"); + expect(scope.variables[1].name).to.be.equal("test"); + expect(scope.references).to.have.length(5); + expect(scope.references[0].identifier.name).to.be.equal("hello"); + expect(scope.references[1].identifier.name).to.be.equal("i"); + expect(scope.references[2].identifier.name).to.be.equal("i"); + expect(scope.references[3].identifier.name).to.be.equal("test"); + expect(scope.references[4].identifier.name).to.be.equal("test"); + }); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/es6-template-literal.js b/packages/eslint-scope/tests/es6-template-literal.js new file mode 100644 index 00000000..23aea3db --- /dev/null +++ b/packages/eslint-scope/tests/es6-template-literal.js @@ -0,0 +1,70 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2014 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("ES6 template literal", () => { + it("refer variables", () => { + const ast = espree(` + (function () { + let i, j, k; + function testing() { } + let template = testing\`testing \${i} and \${j}\` + return template; + }()); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionExpression"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(6); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("i"); + expect(scope.variables[2].name).to.be.equal("j"); + expect(scope.variables[3].name).to.be.equal("k"); + expect(scope.variables[4].name).to.be.equal("testing"); + expect(scope.variables[5].name).to.be.equal("template"); + expect(scope.references).to.have.length(5); + expect(scope.references[0].identifier.name).to.be.equal("template"); + expect(scope.references[1].identifier.name).to.be.equal("testing"); + expect(scope.references[2].identifier.name).to.be.equal("i"); + expect(scope.references[3].identifier.name).to.be.equal("j"); + expect(scope.references[4].identifier.name).to.be.equal("template"); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/export-star-as-ns-from-source.js b/packages/eslint-scope/tests/export-star-as-ns-from-source.js new file mode 100644 index 00000000..487ca3d5 --- /dev/null +++ b/packages/eslint-scope/tests/export-star-as-ns-from-source.js @@ -0,0 +1,35 @@ +import assert from "node:assert"; +import * as espree from "espree"; +import { KEYS } from "eslint-visitor-keys"; +import { analyze } from "../lib/index.js"; + +describe("export * as ns from 'source'", () => { + let scopes; + + beforeEach(() => { + const ast = espree.parse("export * as ns from 'source'", { + ecmaVersion: 2020, + sourceType: "module" + }); + const manager = analyze(ast, { + ecmaVersion: 11, + sourceType: "module", + childVisitorKeys: KEYS + }); + + scopes = [manager.globalScope, ...manager.globalScope.childScopes]; + }); + + it("should not have any references", () => { + for (const scope of scopes) { + assert.strictEqual(scope.references.length, 0, scope.type); + assert.strictEqual(scope.through.length, 0, scope.type); + } + }); + + it("should not have any variables", () => { + for (const scope of scopes) { + assert.strictEqual(scope.variables.length, 0, scope.type); + } + }); +}); diff --git a/packages/eslint-scope/tests/fallback.js b/packages/eslint-scope/tests/fallback.js new file mode 100644 index 00000000..cfbc6a3e --- /dev/null +++ b/packages/eslint-scope/tests/fallback.js @@ -0,0 +1,63 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2016 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("fallback option", () => { + it("should raise an error when it encountered an unknown node if no fallback.", () => { + const ast = espree(` + var foo = 0; + `); + + ast.body[0].declarations[0].init.type = "NumericLiteral"; + + expect(() => { + analyze(ast, { fallback: "none" }); + }).to.throw("Unknown node type NumericLiteral"); + }); + + it("should not raise an error even if it encountered an unknown node when fallback is iteration.", () => { + const ast = espree(` + var foo = 0; + `); + + ast.body[0].declarations[0].init.type = "NumericLiteral"; + + analyze(ast); // default is `fallback: "iteration"` + analyze(ast, { fallback: "iteration" }); + }); + + it("should not raise an error even if it encountered an unknown node when fallback is a function.", () => { + const ast = espree(` + var foo = 0; + `); + + ast.body[0].declarations[0].init.type = "NumericLiteral"; + + analyze(ast, { fallback: node => Object.keys(node) }); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/function-expression-name.js b/packages/eslint-scope/tests/function-expression-name.js new file mode 100644 index 00000000..54445ce7 --- /dev/null +++ b/packages/eslint-scope/tests/function-expression-name.js @@ -0,0 +1,67 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("function name", () => { + it("should create its special scope", () => { + const ast = espree(` + (function name() { + }()); + `); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(3); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + expect(globalScope.isArgumentsMaterialized()).to.be.true; + + // Function expression name scope + let scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function-expression-name"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("name"); + expect(scope.isArgumentsMaterialized()).to.be.true; + expect(scope.references).to.have.length(0); + expect(scope.upper === globalScope).to.be.true; + + // Function scope + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.isArgumentsMaterialized()).to.be.false; + expect(scope.references).to.have.length(0); + expect(scope.upper === scopeManager.scopes[1]).to.be.true; + }); +}); + + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/get-declared-variables.js b/packages/eslint-scope/tests/get-declared-variables.js new file mode 100644 index 00000000..1fd8253f --- /dev/null +++ b/packages/eslint-scope/tests/get-declared-variables.js @@ -0,0 +1,276 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Toru Nagashima +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import esrecurse from "esrecurse"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +const { visit } = esrecurse; + +describe("ScopeManager.prototype.getDeclaredVariables", () => { + + + /** + * Asserts that nodes of the given type declare expected variables. + * @param {Object} ast The AST to check. + * @param {string} type The node type to check. + * @param {string[][]} expectedNamesList List of expected variable names for each node of the given type. + * @throws {Error} If the list of declared variables doesn't match the expected list. + * @returns {void} + */ + function verify(ast, type, expectedNamesList) { + const scopeManager = analyze(ast, { + ecmaVersion: 6, + sourceType: "module" + }); + + visit(ast, { + [type](node) { + const expected = expectedNamesList.shift(); + const actual = scopeManager.getDeclaredVariables(node); + + expect(actual).to.have.length(expected.length); + if (actual.length > 0) { + const end = actual.length - 1; + + for (let i = 0; i <= end; i++) { + expect(actual[i].name).to.be.equal(expected[i]); + } + } + + this.visitChildren(node); + } + }); + + expect(expectedNamesList).to.have.length(0); + } + + it("should get variables that declared on `VariableDeclaration`", () => { + const ast = espree(` + var {a, x: [b], y: {c = 0}} = foo; + let {d, x: [e], y: {f = 0}} = foo; + const {g, x: [h], y: {i = 0}} = foo, {j, k = function() { let l; }} = bar; + `); + + verify(ast, "VariableDeclaration", [ + ["a", "b", "c"], + ["d", "e", "f"], + ["g", "h", "i", "j", "k"], + ["l"] + ]); + }); + + + it("should get variables that declared on `VariableDeclaration` in for-in/of", () => { + const ast = espree(` + for (var {a, x: [b], y: {c = 0}} in foo) { + let g; + } + for (let {d, x: [e], y: {f = 0}} of foo) { + let h; + } + `); + + verify(ast, "VariableDeclaration", [ + ["a", "b", "c"], + ["g"], + ["d", "e", "f"], + ["h"] + ]); + }); + + + it("should get variables that declared on `VariableDeclarator`", () => { + const ast = espree(` + var {a, x: [b], y: {c = 0}} = foo; + let {d, x: [e], y: {f = 0}} = foo; + const {g, x: [h], y: {i = 0}} = foo, {j, k = function() { let l; }} = bar; + `); + + verify(ast, "VariableDeclarator", [ + ["a", "b", "c"], + ["d", "e", "f"], + ["g", "h", "i"], + ["j", "k"], + ["l"] + ]); + }); + + + it("should get variables that declared on `FunctionDeclaration`", () => { + const ast = espree(` + function foo({a, x: [b], y: {c = 0}}, [d, e]) { + let z; + } + function bar({f, x: [g], y: {h = 0}}, [i, j = function(q) { let w; }]) { + let z; + } + `); + + verify(ast, "FunctionDeclaration", [ + ["foo", "a", "b", "c", "d", "e"], + ["bar", "f", "g", "h", "i", "j"] + ]); + }); + + + it("should get variables that declared on `FunctionExpression`", () => { + const ast = espree(` + (function foo({a, x: [b], y: {c = 0}}, [d, e]) { + let z; + }); + (function bar({f, x: [g], y: {h = 0}}, [i, j = function(q) { let w; }]) { + let z; + }); + `); + + verify(ast, "FunctionExpression", [ + ["foo", "a", "b", "c", "d", "e"], + ["bar", "f", "g", "h", "i", "j"], + ["q"] + ]); + }); + + + it("should get variables that declared on `ArrowFunctionExpression`", () => { + const ast = espree(` + (({a, x: [b], y: {c = 0}}, [d, e]) => { + let z; + }); + (({f, x: [g], y: {h = 0}}, [i, j]) => { + let z; + }); + `); + + verify(ast, "ArrowFunctionExpression", [ + ["a", "b", "c", "d", "e"], + ["f", "g", "h", "i", "j"] + ]); + }); + + + it("should get variables that declared on `ClassDeclaration`", () => { + const ast = espree(` + class A { foo(x) { let y; } } + class B { foo(x) { let y; } } + `); + + verify(ast, "ClassDeclaration", [ + ["A", "A"], // outer scope"s and inner scope"s. + ["B", "B"] + ]); + }); + + + it("should get variables that declared on `ClassExpression`", () => { + const ast = espree(` + (class A { foo(x) { let y; } }); + (class B { foo(x) { let y; } }); + `); + + verify(ast, "ClassExpression", [ + ["A"], + ["B"] + ]); + }); + + + it("should get variables that declared on `CatchClause`", () => { + const ast = espree(` + try {} catch ({a, b}) { + let x; + try {} catch ({c, d}) { + let y; + } + } + `); + + verify(ast, "CatchClause", [ + ["a", "b"], + ["c", "d"] + ]); + }); + + + it("should get variables that declared on `ImportDeclaration`", () => { + const ast = espree(` + import "aaa"; + import * as a from "bbb"; + import b, {c, x as d} from "ccc";`); + + verify(ast, "ImportDeclaration", [ + [], + ["a"], + ["b", "c", "d"] + ]); + }); + + + it("should get variables that declared on `ImportSpecifier`", () => { + const ast = espree(` + import "aaa"; + import * as a from "bbb"; + import b, {c, x as d} from "ccc";`); + + verify(ast, "ImportSpecifier", [ + ["c"], + ["d"] + ]); + }); + + + it("should get variables that declared on `ImportDefaultSpecifier`", () => { + const ast = espree(` + import "aaa"; + import * as a from "bbb"; + import b, {c, x as d} from "ccc";`); + + verify(ast, "ImportDefaultSpecifier", [ + ["b"] + ]); + }); + + + it("should get variables that declared on `ImportNamespaceSpecifier`", () => { + const ast = espree(` + import "aaa"; + import * as a from "bbb"; + import b, {c, x as d} from "ccc";`); + + verify(ast, "ImportNamespaceSpecifier", [ + ["a"] + ]); + }); + + + it("should not get duplicate even if it's declared twice", () => { + const ast = espree("var a = 0, a = 1;"); + + verify(ast, "VariableDeclaration", [ + ["a"] + ]); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/global-increment.js b/packages/eslint-scope/tests/global-increment.js new file mode 100644 index 00000000..c4e86349 --- /dev/null +++ b/packages/eslint-scope/tests/global-increment.js @@ -0,0 +1,44 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("global increment", () => { + it("becomes read/write", () => { + const ast = espree("b++;"); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(1); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(1); + expect(globalScope.references[0].isReadWrite()).to.be.true; + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/implicit-global-reference.js b/packages/eslint-scope/tests/implicit-global-reference.js new file mode 100644 index 00000000..4c869ff1 --- /dev/null +++ b/packages/eslint-scope/tests/implicit-global-reference.js @@ -0,0 +1,212 @@ +// Copyright (C) 2013 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("implicit global reference", () => { + it("assignments global scope", () => { + const ast = espree(` + var x = 20; + x = 300; + `); + + const scopes = analyze(ast).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.defs.map(def => def.type)))).to.be.eql( + [ + [ + [ + "Variable" + ] + ] + ] + ); + + expect(scopes[0].implicit.variables.map(variable => variable.name)).to.be.eql([]); + }); + + it("assignments global scope without definition", () => { + const ast = espree(` + x = 300; + x = 300; + `); + + const scopes = analyze(ast).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.defs.map(def => def.type)))).to.be.eql( + [ + [ + ] + ] + ); + + expect(scopes[0].implicit.variables.map(variable => variable.name)).to.be.eql( + [ + "x" + ] + ); + }); + + it("assignments global scope without definition eval", () => { + const ast = espree(` + function inner() { + eval(str); + x = 300; + } + `); + + const scopes = analyze(ast).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.defs.map(def => def.type)))).to.be.eql( + [ + [ + [ + "FunctionName" + ] + ], + [ + [ + ] + ] + ] + ); + + expect(scopes[0].implicit.variables.map(variable => variable.name)).to.be.eql([]); + }); + + it("assignment leaks", () => { + const ast = espree(` + function outer() { + x = 20; + } + `); + + const scopes = analyze(ast).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.name))).to.be.eql( + [ + [ + "outer" + ], + [ + "arguments" + ] + ] + ); + + expect(scopes[0].implicit.variables.map(variable => variable.name)).to.be.eql( + [ + "x" + ] + ); + }); + + it("assignment doesn't leak", () => { + const ast = espree(` + function outer() { + function inner() { + x = 20; + } + var x; + } + `); + + const scopes = analyze(ast).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.name))).to.be.eql( + [ + [ + "outer" + ], + [ + "arguments", + "inner", + "x" + ], + [ + "arguments" + ] + ] + ); + + expect(scopes[0].implicit.variables.map(variable => variable.name)).to.be.eql([]); + }); + + it("for-in-statement leaks", () => { + const ast = espree(` + function outer() { + for (x in y) { } + }`); + + const scopes = analyze(ast).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.name))).to.be.eql( + [ + [ + "outer" + ], + [ + "arguments" + ] + ] + ); + + expect(scopes[0].implicit.variables.map(variable => variable.name)).to.be.eql( + [ + "x" + ] + ); + }); + + it("for-in-statement doesn't leaks", () => { + const ast = espree(` + function outer() { + function inner() { + for (x in y) { } + } + var x; + } + `); + + const scopes = analyze(ast).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.name))).to.be.eql( + [ + [ + "outer" + ], + [ + "arguments", + "inner", + "x" + ], + [ + "arguments" + ] + ] + ); + + expect(scopes[0].implicit.variables.map(variable => variable.name)).to.be.eql([]); + }); +}); diff --git a/packages/eslint-scope/tests/implied-strict.js b/packages/eslint-scope/tests/implied-strict.js new file mode 100644 index 00000000..cc4bcf6f --- /dev/null +++ b/packages/eslint-scope/tests/implied-strict.js @@ -0,0 +1,133 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { getSupportedEcmaVersions } from "./util/ecma-version.js"; +import { analyze } from "../lib/index.js"; + +describe("impliedStrict option", () => { + it("ensures all user scopes are strict if ecmaVersion >= 5", () => { + const ast = espree(` + function foo() { + function bar() { + "use strict"; + } + } + `); + + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const scopeManager = analyze(ast, { ecmaVersion, impliedStrict: true }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.true; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionDeclaration"); + expect(scope.isStrict).to.be.true; + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionDeclaration"); + expect(scope.isStrict).to.be.true; + }); + }); + + it("ensures impliedStrict option is only effective when ecmaVersion option >= 5", () => { + const ast = espree(` + function foo() {} + `); + + const scopeManager = analyze(ast, { ecmaVersion: 3, impliedStrict: true }); + + expect(scopeManager.scopes).to.have.length(2); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionDeclaration"); + expect(scope.isStrict).to.be.false; + }); + + it("omits a nodejs global scope when ensuring all user scopes are strict", () => { + const ast = espree(` + function foo() {} + `); + + const scopeManager = analyze(ast, { ecmaVersion: 5, nodejsScope: true, impliedStrict: true }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.true; + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionDeclaration"); + expect(scope.isStrict).to.be.true; + }); + + it("omits a module global scope when ensuring all user scopes are strict", () => { + const ast = espree("function foo() {}"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, impliedStrict: true, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(3); + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("module"); + expect(scope.isStrict).to.be.true; + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("FunctionDeclaration"); + expect(scope.isStrict).to.be.true; + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/label.js b/packages/eslint-scope/tests/label.js new file mode 100644 index 00000000..3d0ff196 --- /dev/null +++ b/packages/eslint-scope/tests/label.js @@ -0,0 +1,75 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("label", () => { + it("should not create variables", () => { + const ast = espree("function bar() { q: for(;;) { break q; } }"); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(2); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(1); + expect(globalScope.variables[0].name).to.be.equal("bar"); + expect(globalScope.references).to.have.length(0); + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.isArgumentsMaterialized()).to.be.false; + expect(scope.references).to.have.length(0); + }); + + it("should count child node references", () => { + const ast = espree(` + var foo = 5; + + label: while (true) { + console.log(foo); + break; + } + `); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(1); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(1); + expect(globalScope.variables[0].name).to.be.equal("foo"); + expect(globalScope.through.length).to.be.equal(3); + expect(globalScope.through[2].identifier.name).to.be.equal("foo"); + expect(globalScope.through[2].isRead()).to.be.true; + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/nodejs-scope.js b/packages/eslint-scope/tests/nodejs-scope.js new file mode 100644 index 00000000..cd75985b --- /dev/null +++ b/packages/eslint-scope/tests/nodejs-scope.js @@ -0,0 +1,115 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("nodejsScope option", () => { + + it("creates a function scope following the global scope immediately when nodejscope: true", () => { + const ast = espree(` + "use strict"; + var hello = 20; + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6, nodejsScope: true }); + + expect(scopeManager.scopes).to.have.length(2); + expect(scopeManager.isGlobalReturn()).to.be.true; + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("hello"); + }); + + it("creates a function scope following the global scope immediately when sourceType:commonjs", () => { + const ast = espree(` + "use strict"; + var hello = 20; + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6, sourceType: "commonjs" }); + + expect(scopeManager.scopes).to.have.length(2); + expect(scopeManager.isGlobalReturn()).to.be.true; + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.true; + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.variables[1].name).to.be.equal("hello"); + }); + + it("creates a function scope following the global scope immediately and creates module scope", () => { + const ast = espree("import {x as v} from 'mod';"); + + const scopeManager = analyze(ast, { ecmaVersion: 6, nodejsScope: true, sourceType: "module" }); + + expect(scopeManager.scopes).to.have.length(3); + expect(scopeManager.isGlobalReturn()).to.be.true; + + let scope = scopeManager.scopes[0]; + + expect(scope.type).to.be.equal("global"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(0); + + scope = scopeManager.scopes[1]; + expect(scope.type).to.be.equal("function"); + expect(scope.block.type).to.be.equal("Program"); + expect(scope.isStrict).to.be.false; + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("module"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("v"); + expect(scope.variables[0].defs[0].type).to.be.equal("ImportBinding"); + expect(scope.references).to.have.length(0); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/object-expression.js b/packages/eslint-scope/tests/object-expression.js new file mode 100644 index 00000000..f3440d74 --- /dev/null +++ b/packages/eslint-scope/tests/object-expression.js @@ -0,0 +1,46 @@ +import { expect } from "chai"; +import { analyze } from "../lib/index.js"; + +describe("object expression", () => { + it("doesn't require property type", () => { + + // Hardcoded AST. Escope adds an extra "Property" + // key/value to ObjectExpressions, so we're not using + // it parse a program string. + const ast = { + type: "Program", + body: [{ + type: "VariableDeclaration", + declarations: [{ + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "a" + }, + init: { + type: "ObjectExpression", + properties: [{ + kind: "init", + key: { + type: "Identifier", + name: "foo" + }, + value: { + type: "Identifier", + name: "a" + } + }] + } + }] + }] + }; + + const scope = analyze(ast).scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("a"); + expect(scope.references[0].identifier.name).to.be.equal("a"); + expect(scope.references[1].identifier.name).to.be.equal("a"); + }); +}); diff --git a/packages/eslint-scope/tests/optimistic.js b/packages/eslint-scope/tests/optimistic.js new file mode 100644 index 00000000..941e7243 --- /dev/null +++ b/packages/eslint-scope/tests/optimistic.js @@ -0,0 +1,85 @@ +// Copyright (C) 2013 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("optimistic", () => { + it("direct call to eval", () => { + const ast = espree(` + function outer() { + eval(str); + var i = 20; + function inner() { + i; + } + } + `); + + const scopes = analyze(ast, { optimistic: true }).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.name))).to.be.eql( + [ + [ + "outer" + ], + [ + "arguments", + "i", + "inner" + ], + [ + "arguments" + ] + ] + ); + }); + + it("with statement", () => { + const ast = espree(` + function outer() { + eval(str); + var i = 20; + with (obj) { + i; + } + } + `, "script"); + + const scopes = analyze(ast, { optimistic: true }).scopes; + + expect(scopes.map(scope => scope.variables.map(variable => variable.name))).to.be.eql( + [ + [ + "outer" + ], + [ + "arguments", + "i" + ], + [ + ] + ] + ); + }); +}); diff --git a/packages/eslint-scope/tests/references.js b/packages/eslint-scope/tests/references.js new file mode 100644 index 00000000..a4776442 --- /dev/null +++ b/packages/eslint-scope/tests/references.js @@ -0,0 +1,628 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Toru Nagashima +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("References:", () => { + describe("When there is a `let` declaration on global,", () => { + it("the reference on global should be resolved.", () => { + const ast = espree("let a = 0;"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(1); + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables[0]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + + it("the reference in functions should be resolved.", () => { + const ast = espree(` + let a = 0; + function foo() { + let b = a; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + + it("the reference in default parameters should be resolved.", () => { + const ast = espree(` + let a = 0; + function foo(b = a) { + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + }); + + describe("When there is a `const` declaration on global,", () => { + it("the reference on global should be resolved.", () => { + const ast = espree("const a = 0;"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(1); + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables[0]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + + it("the reference in functions should be resolved.", () => { + const ast = espree(` + const a = 0; + function foo() { + const b = a; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + }); + + describe("When there is a `var` declaration on global,", () => { + it("the reference on global should NOT be resolved.", () => { + const ast = espree("var a = 0;"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(1); + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.be.null; + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + + it("the reference in functions should NOT be resolved.", () => { + const ast = espree(` + var a = 0; + function foo() { + var b = a; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.be.null; + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + }); + + describe("When there is a `function` declaration on global,", () => { + it("the reference on global should NOT be resolved.", () => { + const ast = espree(` + function a() {} + a(); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, a] + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.be.null; + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + + it("the reference in functions should NOT be resolved.", () => { + const ast = espree(` + function a() {} + function foo() { + let b = a(); + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, a, foo] + + const scope = scopeManager.scopes[2]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.be.null; + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + }); + + describe("When there is a `class` declaration on global,", () => { + it("the reference on global should be resolved.", () => { + const ast = espree(` + class A {} + let b = new A(); + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, A] + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // [A, b] + expect(scope.references).to.have.length(2); // [b, A] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("A"); + expect(reference.resolved).to.equal(scope.variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + + it("the reference in functions should be resolved.", () => { + const ast = espree(` + class A {} + function foo() { + let b = new A(); + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, A, foo] + + const scope = scopeManager.scopes[2]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, A] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("A"); + expect(reference.resolved).to.equal(scopeManager.scopes[0].variables[0]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + }); + + describe("When there is a `let` declaration in functions,", () => { + it("the reference on the function should be resolved.", () => { + const ast = espree(` + function foo() { + let a = 0; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(2); // [arguments, a] + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables[1]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + + it("the reference in nested functions should be resolved.", () => { + const ast = espree(` + function foo() { + let a = 0; + function bar() { + let b = a; + } + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, foo, bar] + + const scope = scopeManager.scopes[2]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[1].variables[1]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + }); + + describe("When there is a `var` declaration in functions,", () => { + it("the reference on the function should be resolved.", () => { + const ast = espree(` + function foo() { + var a = 0; + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(2); // [global, foo] + + const scope = scopeManager.scopes[1]; + + expect(scope.variables).to.have.length(2); // [arguments, a] + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables[1]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + + it("the reference in nested functions should be resolved.", () => { + const ast = espree(` + function foo() { + var a = 0; + function bar() { + var b = a; + } + } + `); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(3); // [global, foo, bar] + + const scope = scopeManager.scopes[2]; + + expect(scope.variables).to.have.length(2); // [arguments, b] + expect(scope.references).to.have.length(2); // [b, a] + + const reference = scope.references[1]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scopeManager.scopes[1].variables[1]); + expect(reference.writeExpr).to.be.undefined; + expect(reference.isWrite()).to.be.false; + expect(reference.isRead()).to.be.true; + }); + }); + + describe("When there is a `let` declaration with destructuring assignment", () => { + it("\"let [a] = [1];\", the reference should be resolved.", () => { + const ast = espree("let [a] = [1];"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(1); + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables[0]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + + it("\"let {a} = {a: 1};\", the reference should be resolved.", () => { + const ast = espree("let {a} = {a: 1};"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(1); + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables[0]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + + it("\"let {a: {a}} = {a: {a: 1}};\", the reference should be resolved.", () => { + const ast = espree("let {a: {a}} = {a: {a: 1}};"); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.have.length(1); + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length(1); + + const reference = scope.references[0]; + + expect(reference.from).to.equal(scope); + expect(reference.identifier.name).to.equal("a"); + expect(reference.resolved).to.equal(scope.variables[0]); + expect(reference.writeExpr).to.not.be.undefined; + expect(reference.isWrite()).to.be.true; + expect(reference.isRead()).to.be.false; + }); + }); + + describe("Reference.init should be a boolean value of whether it is one to initialize or not.", () => { + const trueCodes = [ + "var a = 0;", + "let a = 0;", + "const a = 0;", + "var [a] = [];", + "let [a] = [];", + "const [a] = [];", + "var [a = 1] = [];", + "let [a = 1] = [];", + "const [a = 1] = [];", + "var {a} = {};", + "let {a} = {};", + "const {a} = {};", + "var {b: a} = {};", + "let {b: a} = {};", + "const {b: a} = {};", + "var {b: a = 0} = {};", + "let {b: a = 0} = {};", + "const {b: a = 0} = {};", + "for (var a in []);", + "for (let a in []);", + "for (var [a] in []);", + "for (let [a] in []);", + "for (var [a = 0] in []);", + "for (let [a = 0] in []);", + "for (var {a} in []);", + "for (let {a} in []);", + "for (var {a = 0} in []);", + "for (let {a = 0} in []);", + "new function(a = 0) {}", + "new function([a = 0] = []) {}", + "new function({b: a = 0} = {}) {}" + ]; + + trueCodes.forEach(code => + it(`"${code}", all references should be true.`, () => { + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.be.length.of.at.least(1); + + const scope = scopeManager.scopes.at(-1); + + expect(scope.variables).to.have.length.of.at.least(1); + expect(scope.references).to.have.length.of.at.least(1); + + scope.references.forEach(reference => { + expect(reference.identifier.name).to.equal("a"); + expect(reference.isWrite()).to.be.true; + expect(reference.init).to.be.true; + }); + })); + + let falseCodes = [ + "let a; a = 0;", + "let a; [a] = [];", + "let a; [a = 1] = [];", + "let a; ({a} = {});", + "let a; ({b: a} = {});", + "let a; ({b: a = 0} = {});", + "let a; for (a in []);", + "let a; for ([a] in []);", + "let a; for ([a = 0] in []);", + "let a; for ({a} in []);", + "let a; for ({a = 0} in []);" + ]; + + falseCodes.forEach(code => + it(`"${code}", all references should be false.`, () => { + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.be.length.of.at.least(1); + + const scope = scopeManager.scopes.at(-1); + + expect(scope.variables).to.have.length(1); + expect(scope.references).to.have.length.of.at.least(1); + + scope.references.forEach(reference => { + expect(reference.identifier.name).to.equal("a"); + expect(reference.isWrite()).to.be.true; + expect(reference.init).to.be.false; + }); + })); + + falseCodes = [ + "let a; let b = a;", + "let a; let [b] = a;", + "let a; let [b = a] = [];", + "let a; for (var b in a);", + "let a; for (var [b = a] in []);", + "let a; for (let b in a);", + "let a; for (let [b = a] in []);", + "let a,b; b = a;", + "let a,b; [b] = a;", + "let a,b; [b = a] = [];", + "let a,b; for (b in a);", + "let a,b; for ([b = a] in []);", + "let a; a.foo = 0;", + "let a,b; b = a.foo;" + ]; + falseCodes.forEach(code => + it(`"${code}", readonly references of "a" should be undefined.`, () => { + const ast = espree(code); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + + expect(scopeManager.scopes).to.be.length.of.at.least(1); + + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length.of.at.least(1); + expect(scope.variables[0].name).to.equal("a"); + + const references = scope.variables[0].references; + + expect(references).to.have.length.of.at.least(1); + + references.forEach(reference => { + expect(reference.isRead()).to.be.true; + expect(reference.init).to.be.undefined; + }); + })); + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tests/typescript.js b/packages/eslint-scope/tests/typescript.js new file mode 100644 index 00000000..aa429270 --- /dev/null +++ b/packages/eslint-scope/tests/typescript.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Typescript scope tests + * @author Reyad Attiyat + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import { expect } from "chai"; +import typescriptEslintParser from "@typescript-eslint/parser"; +import { analyze } from "../lib/index.js"; + +const { parse } = typescriptEslintParser; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("typescript", () => { + describe("multiple call signatures", () => { + it("should create a function scope", () => { + const ast = parse(` + function foo(bar: number): number; + function foo(bar: string): string; + function foo(bar: string | number): string | number { + return bar; + } + `, { + range: true + }); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(2); + + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(1); + expect(globalScope.references).to.have.length(4); + expect(globalScope.isArgumentsMaterialized()).to.be.true; + + const scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(2); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.isArgumentsMaterialized()).to.be.false; + expect(scope.references).to.have.length(1); + }); + }); +}); diff --git a/packages/eslint-scope/tests/use-strict.js b/packages/eslint-scope/tests/use-strict.js new file mode 100644 index 00000000..557fd583 --- /dev/null +++ b/packages/eslint-scope/tests/use-strict.js @@ -0,0 +1,281 @@ +/** + * @fileoverview Tests for "use strict" directives. + * @author Milos Djermanovic + */ + +import assert from "node:assert"; +import * as espree from "espree"; +import { KEYS } from "eslint-visitor-keys"; +import { analyze } from "../lib/index.js"; +import { getSupportedEcmaVersions } from "./util/ecma-version.js"; + +/** + * Asserts `isStrict` property value for the given scope and all its descendants. + * @param {Scope} scope The scope to check. + * @param {boolean} expected The expected value for `isStrict` property. + * @throws {AssertionError} If `isStrict` property value of `scope` or of + * any of its descendant scopes doesn't match `expected`. + * @returns {void} + */ +function assertIsStrictRecursively(scope, expected) { + assert.strictEqual(scope.isStrict, expected); + + scope.childScopes.forEach(childScope => { + assertIsStrictRecursively(childScope, expected); + }); +} + +describe("'use strict' directives", () => { + + it("should be ignored when ecmaVersion = 3", () => { + const ecmaVersion = 3; + + const ast = espree.parse(` + "use strict"; + function a() { + "use strict"; + function b() { + foo(); + } + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assertIsStrictRecursively(globalScope, false); + }); + + it("at the top level should make all scopes strict when ecmaVersion >= 5", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + "use strict"; + if (a) { + foo(); + } + function b() { + if (c) { + foo(); + } + function d() { + if (e) { + foo(); + } + } + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assertIsStrictRecursively(globalScope, true); + }); + }); + + it("at the function level should make the function's scope and all its descendants strict when ecmaVersion >= 5", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function a() { + "use strict"; + if (b) { + foo(); + } + function c() { + if (d) { + foo(); + } + } + } + function e() { + if (f) { + foo(); + } + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assertIsStrictRecursively(globalScope.childScopes[0], true); // function a() { ... } + assertIsStrictRecursively(globalScope.childScopes[1], false); // function e() { ... } + }); + }); + + it("can be with single quotes at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + 'use strict'; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, true); + }); + }); + + it("can be without the semicolon at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + "use strict" + foo() + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, true); + }); + }); + + it("can be anywhere in the directive prologue at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + "foo"; + "use strict"; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, true); + }); + }); + + it("cannot be after the directive prologue at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + foo(); + "use strict"; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + }); + }); + + it("cannot contain escapes at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + "use \\strict"; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + }); + }); + + it("cannot be parenthesized at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + ("use strict"); + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + }); + }); + + it("can be with single quotes in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + 'use strict'; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, true); + }); + }); + + it("can be without the semicolon in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + "use strict" + bar() + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, true); + }); + }); + + it("can be anywhere in the directive prologue in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + "foo"; + "use strict"; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, true); + }); + }); + + it("cannot be after the directive prologue in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + bar(); + "use strict"; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, false); + }); + }); + + it("cannot contain escapes in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + "use \\strict"; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, false); + }); + }); + + it("cannot be parenthesized in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + ("use strict"); + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, false); + }); + }); +}); diff --git a/packages/eslint-scope/tests/util/ecma-version.js b/packages/eslint-scope/tests/util/ecma-version.js new file mode 100644 index 00000000..22cc674d --- /dev/null +++ b/packages/eslint-scope/tests/util/ecma-version.js @@ -0,0 +1,29 @@ +/** + * @fileoverview ECMAScript version utilities used in tests + * @author Milos Djermanovic + */ + +import * as espree from "espree"; + +/** + * Gets an array of supported ECMAScript versions. + * @param {number} [min] Minimum ECMAScript version to get. This should be a revision-based number (3, 5, 6, 7...). + * @returns {number[]} Supported ECMAScript versions, including their year-based representations. + * @example + * + * getSupportedEcmaVersions() + * // => [3, 5, 6, 2015, 7, 2016, 8, 2017, 9, 2018, 10, 2019, 11, 2020, 12, 2021, 13, 2022] + * + * getSupportedEcmaVersions({ min: 8 }) + * // => [8, 2017, 9, 2018, 10, 2019, 11, 2020, 12, 2021, 13, 2022] + * + */ +export function getSupportedEcmaVersions({ min = 0 } = {}) { + return espree.supportedEcmaVersions + .filter( + ecmaVersion => ecmaVersion >= min + ) + .flatMap( + ecmaVersion => (ecmaVersion >= 6 ? [ecmaVersion, ecmaVersion + 2009] : [ecmaVersion]) + ); +} diff --git a/packages/eslint-scope/tests/util/espree.js b/packages/eslint-scope/tests/util/espree.js new file mode 100644 index 00000000..3e3de7a0 --- /dev/null +++ b/packages/eslint-scope/tests/util/espree.js @@ -0,0 +1,41 @@ +/* + Copyright (C) 2015 Yusuke Suzuki + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import * as espree from "espree"; + +/** + * Parse into Espree AST. + * @param {string} code The code + * @param {"module"|"script"} [sourceType="module"] The source type + * @returns {Object} The parsed Espree AST + */ +export default function(code, sourceType = "module") { + return espree.parse(code, { + range: true, + ecmaVersion: 7, + sourceType + }); +} + +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/packages/eslint-scope/tests/with-scope.js b/packages/eslint-scope/tests/with-scope.js new file mode 100644 index 00000000..fc21010f --- /dev/null +++ b/packages/eslint-scope/tests/with-scope.js @@ -0,0 +1,65 @@ +// -*- coding: utf-8 -*- +// Copyright (C) 2015 Yusuke Suzuki +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("with", () => { + it("creates scope", () => { + const ast = espree(` + (function () { + with (obj) { + testing; + } + }()); + `, "script"); + + const scopeManager = analyze(ast); + + expect(scopeManager.scopes).to.have.length(3); + const globalScope = scopeManager.scopes[0]; + + expect(globalScope.type).to.be.equal("global"); + expect(globalScope.variables).to.have.length(0); + expect(globalScope.references).to.have.length(0); + + let scope = scopeManager.scopes[1]; + + expect(scope.type).to.be.equal("function"); + expect(scope.variables).to.have.length(1); + expect(scope.variables[0].name).to.be.equal("arguments"); + expect(scope.isArgumentsMaterialized()).to.be.false; + expect(scope.references).to.have.length(1); + expect(scope.references[0].resolved).to.be.null; + + scope = scopeManager.scopes[2]; + expect(scope.type).to.be.equal("with"); + expect(scope.variables).to.have.length(0); + expect(scope.isArgumentsMaterialized()).to.be.true; + expect(scope.references).to.have.length(1); + expect(scope.references[0].resolved).to.be.null; + }); +}); + +// vim: set sw=4 ts=4 et tw=80 : diff --git a/packages/eslint-scope/tools/update-version.js b/packages/eslint-scope/tools/update-version.js new file mode 100644 index 00000000..f25f05e0 --- /dev/null +++ b/packages/eslint-scope/tools/update-version.js @@ -0,0 +1,18 @@ +/** + * @fileoverview Script to update lib/version.js to the value in package.json + * @author Nicholas C. Zakas + */ + +import fs from "node:fs"; + +/* + * IMPORTANT: This must be run *before* Rollup so the built package will have + * the correct version number exported. + * + * This is necessary because ESM can't import JSON files directly and we want + * this value to be available in the browser as well as in Node.js. + */ + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); + +fs.writeFileSync("lib/version.js", `const version = "${pkg.version}";\n\nexport default version;\n`); diff --git a/packages/espree/README.md b/packages/espree/README.md index 748366ea..af8ab371 100644 --- a/packages/espree/README.md +++ b/packages/espree/README.md @@ -242,3 +242,22 @@ See [finished-proposals.md](https://github.com/tc39/proposals/blob/master/finish ### How do you determine which experimental features to support? In general, we do not support experimental JavaScript features. We may make exceptions from time to time depending on the maturity of the features. + +## Sponsors + +The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate) to get your logo on our README and website. + + + +

Platinum Sponsors

+

Automattic Airbnb

Gold Sponsors

+

Eli Schleifer Salesforce

Silver Sponsors

+

JetBrains Liftoff American Express Workleap

Bronze Sponsors

+

notion Anagram Solver Icons8 Discord Ignition Nx HeroCoders Nextbase Starter Kit

+ + + +

Technology Sponsors

+

Netlify Algolia 1Password +

+ diff --git a/packages/espree/package.json b/packages/espree/package.json index 5d64b332..475079aa 100644 --- a/packages/espree/package.json +++ b/packages/espree/package.json @@ -41,7 +41,6 @@ "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^11.2.0", "c8": "^7.11.0", - "chai": "^4.3.6", "eslint-release": "^3.2.0", "esprima-fb": "^8001.2001.0-dev-harmony-fb", "mocha": "^9.2.2", diff --git a/release-please-config.json b/release-please-config.json index 9959f055..cdf3e031 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,6 +5,9 @@ "packages": { "packages/espree": { "release-type": "node" + }, + "packages/eslint-scope": { + "release-type": "node" } } }