diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index fa6bf7af..702a9778 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -46,7 +46,7 @@ jobs: # fewest internal dependencies are released first and the packages with the # most internal dependencies are released last. #----------------------------------------------------------------------------- - + #----------------------------------------------------------------------------- # eslint-visitor-keys #----------------------------------------------------------------------------- @@ -57,21 +57,19 @@ jobs: env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - - name: Tweet Release Announcement - run: npx @humanwhocodes/tweet "eslint-visitor-keys v${{ steps.release.outputs['packages/eslint-visitor-keys--major'] }}.${{ steps.release.outputs['packages/eslint-visitor-keys--minor'] }}.${{ steps.release.outputs['packages/eslint-visitor-keys--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/eslint-visitor-keys--tag_name'] }}" + - name: Post Release Announcement + run: npx @humanwhocodes/crosspost -t -b -m "eslint-visitor-keys v${{ steps.release.outputs['packages/eslint-visitor-keys--major'] }}.${{ steps.release.outputs['packages/eslint-visitor-keys--minor'] }}.${{ steps.release.outputs['packages/eslint-visitor-keys--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/eslint-visitor-keys--tag_name'] }}" if: ${{ steps.release.outputs['packages/eslint-visitor-keys--release_created'] }} env: - TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} - TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_API_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_API_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-visitor-keys v${{ steps.release.outputs['packages/eslint-visitor-keys--major'] }}.${{ steps.release.outputs['packages/eslint-visitor-keys--minor'] }}.${{ steps.release.outputs['packages/eslint-visitor-keys--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/eslint-visitor-keys--tag_name'] }}" - if: ${{ steps.release.outputs['packages/eslint-visitor-keys--release_created'] }} - env: MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} MASTODON_HOST: ${{ secrets.MASTODON_HOST }} + BLUESKY_IDENTIFIER: ${{ vars.BLUESKY_IDENTIFIER }} + BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} + BLUESKY_HOST: ${{ vars.BLUESKY_HOST }} #----------------------------------------------------------------------------- # espree @@ -84,20 +82,18 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - name: Tweet Release Announcement - run: npx @humanwhocodes/tweet "espree v${{ steps.release.outputs['packages/espree--major'] }}.${{ steps.release.outputs['packages/espree--minor'] }}.${{ steps.release.outputs['packages/espree--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/espree--tag_name'] }}" + run: npx @humanwhocodes/crosspost -t -b -m "espree v${{ steps.release.outputs['packages/espree--major'] }}.${{ steps.release.outputs['packages/espree--minor'] }}.${{ steps.release.outputs['packages/espree--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/espree--tag_name'] }}" if: ${{ steps.release.outputs['packages/espree--release_created'] }} env: - TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} - TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_API_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_API_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 "espree v${{ steps.release.outputs['packages/espree--major'] }}.${{ steps.release.outputs['packages/espree--minor'] }}.${{ steps.release.outputs['packages/espree--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/espree--tag_name'] }}" - if: ${{ steps.release.outputs['packages/espree--release_created'] }} - env: MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} MASTODON_HOST: ${{ secrets.MASTODON_HOST }} + BLUESKY_IDENTIFIER: ${{ vars.BLUESKY_IDENTIFIER }} + BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} + BLUESKY_HOST: ${{ vars.BLUESKY_HOST }} #----------------------------------------------------------------------------- # eslint-scope @@ -110,17 +106,15 @@ jobs: 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'] }}" + run: npx @humanwhocodes/crosspost -t -b -m "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_API_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_API_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 }} + BLUESKY_IDENTIFIER: ${{ vars.BLUESKY_IDENTIFIER }} + BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} + BLUESKY_HOST: ${{ vars.BLUESKY_HOST }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ff50f2d2..8fc408ad 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { "packages/espree": "10.3.0", - "packages/eslint-scope": "8.2.0", + "packages/eslint-scope": "8.3.0", "packages/eslint-visitor-keys": "4.2.0" } diff --git a/README.md b/README.md index 274b729f..116a62e5 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

trunk.io

Silver Sponsors

-

JetBrains Liftoff American Express Workleap

Bronze Sponsors

-

WordHint Anagram Solver Icons8 Discord GitBook Nx HeroCoders

+

Qlty Software trunk.io Shopify

Silver Sponsors

+

Vite JetBrains Liftoff StackBlitz

Bronze Sponsors

+

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

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/eslint-scope/CHANGELOG.md b/packages/eslint-scope/CHANGELOG.md index 1882cc54..fc5f1f85 100644 --- a/packages/eslint-scope/CHANGELOG.md +++ b/packages/eslint-scope/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [8.3.0](https://github.com/eslint/js/compare/eslint-scope-v8.2.0...eslint-scope-v8.3.0) (2025-03-07) + + +### Features + +* Option to track JSX components as references ([#646](https://github.com/eslint/js/issues/646)) ([6dd3cbc](https://github.com/eslint/js/commit/6dd3cbc2aaa285736eb668e4763a6c1d58f0fb59)) + ## [8.2.0](https://github.com/eslint/js/compare/eslint-scope-v8.1.0...eslint-scope-v8.2.0) (2024-10-29) diff --git a/packages/eslint-scope/README.md b/packages/eslint-scope/README.md index e60b4db3..2896d81e 100644 --- a/packages/eslint-scope/README.md +++ b/packages/eslint-scope/README.md @@ -37,6 +37,7 @@ In order to analyze scope, you'll need to have an [ESTree](https://github.com/es * `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/js/tree/main/packages/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. + * `jsx` (default: `false`) - Enables the tracking of JSX components as variable references. Example: @@ -99,9 +100,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

trunk.io

Silver Sponsors

-

JetBrains Liftoff American Express Workleap

Bronze Sponsors

-

WordHint Anagram Solver Icons8 Discord GitBook Nx HeroCoders

+

Qlty Software trunk.io Shopify

Silver Sponsors

+

Vite JetBrains Liftoff StackBlitz

Bronze Sponsors

+

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

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/eslint-scope/lib/index.js b/packages/eslint-scope/lib/index.js index 7e79c923..85f8b510 100644 --- a/packages/eslint-scope/lib/index.js +++ b/packages/eslint-scope/lib/index.js @@ -121,6 +121,7 @@ function updateDeeply(target, override) { * (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 {boolean} [providedOptions.jsx=false] support JSX references * @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 diff --git a/packages/eslint-scope/lib/referencer.js b/packages/eslint-scope/lib/referencer.js index f939aa83..ad88ea20 100644 --- a/packages/eslint-scope/lib/referencer.js +++ b/packages/eslint-scope/lib/referencer.js @@ -649,6 +649,58 @@ class Referencer extends esrecurse.Visitor { // do nothing. } + + JSXIdentifier(node) { + + // Special case: "this" should not count as a reference + if (this.scopeManager.__isJSXEnabled() && node.name !== "this") { + this.currentScope().__referencing(node); + } + } + + JSXMemberExpression(node) { + this.visit(node.object); + } + + JSXElement(node) { + if (this.scopeManager.__isJSXEnabled()) { + this.visit(node.openingElement); + node.children.forEach(this.visit, this); + } else { + this.visitChildren(node); + } + } + + JSXOpeningElement(node) { + if (this.scopeManager.__isJSXEnabled()) { + + const nameNode = node.name; + const isComponentName = nameNode.type === "JSXIdentifier" && nameNode.name[0].toUpperCase() === nameNode.name[0]; + const isComponent = isComponentName || nameNode.type === "JSXMemberExpression"; + + // we only want to visit JSXIdentifier nodes if they are capitalized + if (isComponent) { + this.visit(nameNode); + } + } + + node.attributes.forEach(this.visit, this); + } + + JSXAttribute(node) { + if (node.value) { + this.visit(node.value); + } + } + + JSXExpressionContainer(node) { + this.visit(node.expression); + } + + JSXNamespacedName(node) { + this.visit(node.namespace); + this.visit(node.name); + } } export default Referencer; diff --git a/packages/eslint-scope/lib/scope-manager.js b/packages/eslint-scope/lib/scope-manager.js index a136648f..b012b646 100644 --- a/packages/eslint-scope/lib/scope-manager.js +++ b/packages/eslint-scope/lib/scope-manager.js @@ -59,6 +59,10 @@ class ScopeManager { return this.__options.ignoreEval; } + __isJSXEnabled() { + return this.__options.jsx === true; + } + isGlobalReturn() { return this.__options.nodejsScope || this.__options.sourceType === "commonjs"; } diff --git a/packages/eslint-scope/lib/scope.js b/packages/eslint-scope/lib/scope.js index b3ef0266..46eeb771 100644 --- a/packages/eslint-scope/lib/scope.js +++ b/packages/eslint-scope/lib/scope.js @@ -421,7 +421,7 @@ class Scope { __referencing(node, assign, writeExpr, maybeImplicitGlobal, partial, init) { // because Array element may be null - if (!node || node.type !== Syntax.Identifier) { + if (!node || (node.type !== Syntax.Identifier && node.type !== "JSXIdentifier")) { return; } diff --git a/packages/eslint-scope/package.json b/packages/eslint-scope/package.json index 759d8ec3..5d7a1977 100644 --- a/packages/eslint-scope/package.json +++ b/packages/eslint-scope/package.json @@ -11,7 +11,7 @@ }, "./package.json": "./package.json" }, - "version": "8.2.0", + "version": "8.3.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, diff --git a/packages/eslint-scope/tests/jsx.js b/packages/eslint-scope/tests/jsx.js new file mode 100644 index 00000000..68692e0b --- /dev/null +++ b/packages/eslint-scope/tests/jsx.js @@ -0,0 +1,463 @@ +/** + * @fileoverview Tests for JSX reference tracking. + * @author Nicholas C. Zakas + */ + +import { expect } from "chai"; +import espree from "./util/espree.js"; +import { analyze } from "../lib/index.js"; + +describe("References:", () => { + + describe("JSX References:", () => { + it("should treat JSX identifiers as references", () => { + const ast = espree(` + const MyComponent = () =>
; + const element = ; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // MyComponent, element + expect(scope.references).to.have.length(3); // MyComponent def, element def, MyComponent use + + const myComponentRef = scope.references[2]; + + expect(myComponentRef.identifier.name).to.equal("MyComponent"); + expect(myComponentRef.isRead()).to.be.true; + expect(myComponentRef.resolved).to.equal(scope.variables[0]); + }); + + it("no JSX equivalent: should treat JSX identifiers as references", () => { + const ast = espree(` + const MyComponent = () => "
"; + const element = MyComponent; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // MyComponent, element + expect(scope.references).to.have.length(3); // MyComponent def, element def, MyComponent use + + const myComponentRef = scope.references[2]; + + expect(myComponentRef.identifier.name).to.equal("MyComponent"); + expect(myComponentRef.isRead()).to.be.true; + expect(myComponentRef.resolved).to.equal(scope.variables[0]); + }); + + it("should not treat JSX identifiers in closing elements as references", () => { + const ast = espree(` + const MyComponent = () =>
; + const element = ; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // MyComponent, element + expect(scope.references).to.have.length(3); // MyComponent def, element def, MyComponent use + + const myComponentRef = scope.references[2]; + + expect(myComponentRef.identifier.name).to.equal("MyComponent"); + expect(myComponentRef.isRead()).to.be.true; + expect(myComponentRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle JSX attributes as references with JSX enabled", () => { + const ast = espree(` + const value = "test"; + const MyComponent = () =>
; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // value, MyComponent + expect(scope.references).to.have.length(2); // value def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // value def, value use + expect(scope.through).to.have.length(0); // attr should not be a reference + + const valueRef = scope.references[0]; + + expect(valueRef.identifier.name).to.equal("value"); + expect(valueRef.isWrite()).to.be.true; + expect(valueRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle JSX attributes as references with JSX disabled", () => { + const ast = espree(` + const value = "test"; + const MyComponent = () =>
; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: false }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // value, MyComponent + expect(scope.references).to.have.length(2); // value def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // value def, value use + expect(scope.through).to.have.length(0); // attr should not be a reference + + const valueRef = scope.references[0]; + + expect(valueRef.identifier.name).to.equal("value"); + expect(valueRef.isWrite()).to.be.true; + expect(valueRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle identifiers in child JSX expression containers with JSX enabled", () => { + const ast = espree(` + const value = "test"; + const MyComponent = () =>
{value}
; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // value, MyComponent + expect(scope.references).to.have.length(2); // value def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // value def, value use + expect(scope.through).to.have.length(0); + + const valueRef = scope.references[0]; + + expect(valueRef.identifier.name).to.equal("value"); + expect(valueRef.isWrite()).to.be.true; + expect(valueRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle identifiers in child JSX expression containers with JSX disabled", () => { + const ast = espree(` + const value = "test"; + const MyComponent = () =>
{value}
; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: false }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // value, MyComponent + expect(scope.references).to.have.length(2); // value def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // value def, value use + expect(scope.through).to.have.length(0); + + const valueRef = scope.references[0]; + + expect(valueRef.identifier.name).to.equal("value"); + expect(valueRef.isWrite()).to.be.true; + expect(valueRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle nested JSX component references", () => { + const ast = espree(` + const Child = () =>
; + const Parent = () => ( +
+ + +
+ ); + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // Child, Parent + expect(scope.references).to.have.length(2); // Child, Parent + expect(scope.variables[0].references).to.have.length(3); // Child def, Child use x2 + + const childRefs = scope.references.filter(ref => ref.identifier.name === "Child"); + + expect(childRefs).to.have.length(1); // 1 def + expect(childRefs[0].isWrite()).to.be.true; + expect(childRefs[0].resolved).to.equal(scope.variables[0]); + }); + + it("should handle JSX fragment references", () => { + const ast = espree(` + const MyComponent = () => ( + <> +
+
+ + ); + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(1); // MyComponent + expect(scope.references).to.have.length(1); // MyComponent + }); + + it("should handle JSX fragments with component children", () => { + const ast = espree(` + const Child = () =>
; + const Parent = () => ( + <> + + + + ); + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // Child, Parent + expect(scope.references).to.have.length(2); // Child, Parent + expect(scope.variables[0].references).to.have.length(3); // Child def, Child use x2 + + const childRefs = scope.references.filter(ref => ref.identifier.name === "Child"); + + expect(childRefs).to.have.length(1); // 1 def + expect(childRefs[0].isWrite()).to.be.true; + expect(childRefs[0].resolved).to.equal(scope.variables[0]); + }); + + it("no JSX equivalent: should handle JSX fragments with component children", () => { + const ast = espree(` + const Child = () =>
; + const Parent = () => ( + [ + Child, + Child + ] + ); + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // Child, Parent + expect(scope.references).to.have.length(2); // Child, Parent + expect(scope.variables[0].references).to.have.length(3); // Child def, Child use x2 + + const childRefs = scope.references.filter(ref => ref.identifier.name === "Child"); + + expect(childRefs).to.have.length(1); // 1 def + expect(childRefs[0].isWrite()).to.be.true; + expect(childRefs[0].resolved).to.equal(scope.variables[0]); + }); + + it("should handle JSX spread attributes", () => { + const ast = espree(` + const props = { attr: "value" }; + const MyComponent = () =>
; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // props, MyComponent + expect(scope.references).to.have.length(2); // props def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // props def, props use + + const propsRef = scope.references[0]; + + expect(propsRef.identifier.name).to.equal("props"); + expect(propsRef.isWrite()).to.be.true; + expect(propsRef.resolved).to.equal(scope.variables[0]); + }); + + it("no JSX equivalent: should handle JSX spread attributes", () => { + const ast = espree(` + const props = { attr: "value" }; + const MyComponent = () => [...props]; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // props, MyComponent + expect(scope.references).to.have.length(2); // props def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // props def, props use + + const propsRef = scope.references[0]; + + expect(propsRef.identifier.name).to.equal("props"); + expect(propsRef.isWrite()).to.be.true; + expect(propsRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle JSX spread attributes with destructuring", () => { + const ast = espree(` + const props = { attr: "value" }; + const MyComponent = ({ attr }) =>
; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // props, MyComponent + expect(scope.references).to.have.length(2); // props def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // props def, props use + + const propsRef = scope.references[0]; + + expect(propsRef.identifier.name).to.equal("props"); + expect(propsRef.isWrite()).to.be.true; + expect(propsRef.resolved).to.equal(scope.variables[0]); + }); + + it("no JSX equivalent: should handle JSX spread attributes with destructuring", () => { + const ast = espree(` + const props = { attr: "value" }; + const MyComponent = ({ attr }) => [...props]; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // props, MyComponent + expect(scope.references).to.have.length(2); // props def, MyComponent def + expect(scope.variables[0].references).to.have.length(2); // props def, props use + + const propsRef = scope.references[0]; + + expect(propsRef.identifier.name).to.equal("props"); + expect(propsRef.isWrite()).to.be.true; + expect(propsRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle JSX syntax", () => { + const ast = espree(` + const obj = { prop: () =>
}; + const element = ; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // obj, element + expect(scope.references).to.have.length(3); // obj def, element def, obj.prop use + + const objRef = scope.references[2]; + + expect(objRef.identifier.name).to.equal("obj"); + expect(objRef.isRead()).to.be.true; + expect(objRef.resolved).to.equal(scope.variables[0]); + }); + + it("no JSX equivalent: should handle JSX syntax", () => { + const ast = espree(` + const obj = { prop: () =>
}; + const element = obj.prop; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // obj, element + expect(scope.references).to.have.length(3); // obj def, element def, obj.prop use + + const objRef = scope.references[2]; + + expect(objRef.identifier.name).to.equal("obj"); + expect(objRef.isRead()).to.be.true; + expect(objRef.resolved).to.equal(scope.variables[0]); + }); + + it("should handle JSX elements with a namespace", () => { + const ast = espree(` + const MyNamespace = {}; + MyNamespace.MyComponent = () =>
; + const element = ; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // MyNamespace, element + expect(scope.references).to.have.length(4); // MyNamespace def, MyComponent def, element def, MyNamespace.MyComponent use + + const myNamespaceRef = scope.references[3]; + + expect(myNamespaceRef.identifier.name).to.equal("MyNamespace"); + expect(myNamespaceRef.isRead()).to.be.true; + expect(myNamespaceRef.resolved).to.equal(scope.variables[0]); + }); + + it("should ignore JSX namespaced names", () => { + const ast = espree(` + const MyNamespace = {}; + const element = ; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(2); // MyNamespace, element + expect(scope.references).to.have.length(2); // MyNamespace def, element def + + const myNamespaceRef = scope.references[0]; + + expect(myNamespaceRef.identifier.name).to.equal("MyNamespace"); + expect(myNamespaceRef.isWrite()).to.be.true; + expect(myNamespaceRef.resolved).to.equal(scope.variables[0]); + }); + + it("should not treat any JSX identifiers as references with JSX disabled", () => { + const ast = espree(` + ; + ; +
; + ; + ; + ; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: false }); + const scope = scopeManager.scopes[0]; + + expect(scope.variables).to.have.length(0); + expect(scope.references).to.have.length(0); + }); + + it("should not treat 'this' as a reference when used in a JSX tagname", () => { + + const ast = espree(` + this.MyComponent = () =>
; + const element = ; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6, jsx: true }); + const scope = scopeManager.scopes[0]; + + expect(scope.references.length).to.equal(1); + expect(scope.variables.length).to.equal(1); + + const elementRef = scope.references[0]; + + expect(elementRef.identifier.name).to.equal("element"); + expect(elementRef.isWrite()).to.be.true; + expect(elementRef.resolved).to.equal(scope.variables[0]); + }); + + it("no JSX equivalent: should not treat 'this' as a reference when used in a JSX tagname", () => { + + const ast = espree(` + this.MyComponent = () =>
; + const element = this.MyComponent; + `, "script", true); + + const scopeManager = analyze(ast, { ecmaVersion: 6 }); + const scope = scopeManager.scopes[0]; + + expect(scope.references.length).to.equal(1); + expect(scope.variables.length).to.equal(1); + + const elementRef = scope.references[0]; + + expect(elementRef.identifier.name).to.equal("element"); + expect(elementRef.isWrite()).to.be.true; + expect(elementRef.resolved).to.equal(scope.variables[0]); + }); + + }); + + +}); diff --git a/packages/eslint-scope/tests/util/espree.js b/packages/eslint-scope/tests/util/espree.js index 3e3de7a0..d005eb1f 100644 --- a/packages/eslint-scope/tests/util/espree.js +++ b/packages/eslint-scope/tests/util/espree.js @@ -28,13 +28,17 @@ import * as espree from "espree"; * Parse into Espree AST. * @param {string} code The code * @param {"module"|"script"} [sourceType="module"] The source type + * @param {boolean} [jsx=false] The flag to enable JSX parsing * @returns {Object} The parsed Espree AST */ -export default function(code, sourceType = "module") { +export default function(code, sourceType = "module", jsx = false) { return espree.parse(code, { range: true, ecmaVersion: 7, - sourceType + sourceType, + ecmaFeatures: { + jsx + } }); } diff --git a/packages/eslint-visitor-keys/README.md b/packages/eslint-visitor-keys/README.md index 3cbbdd39..3bb0e93b 100644 --- a/packages/eslint-visitor-keys/README.md +++ b/packages/eslint-visitor-keys/README.md @@ -111,9 +111,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

trunk.io

Silver Sponsors

-

JetBrains Liftoff American Express Workleap

Bronze Sponsors

-

WordHint Anagram Solver Icons8 Discord GitBook Nx HeroCoders

+

Qlty Software trunk.io Shopify

Silver Sponsors

+

Vite JetBrains Liftoff StackBlitz

Bronze Sponsors

+

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

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-extends-type-reference.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/bad-extends-type-reference.d.ts deleted file mode 100644 index 1457d679..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-extends-type-reference.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Something extends BadSomething { - type: "Something"; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-parameters.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-parameters.d.ts deleted file mode 100644 index 0095c9c2..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-parameters.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Statement {} - -export interface StaticBlock extends BadTypeParam { - type: "StaticBlock"; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-reference.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-reference.d.ts deleted file mode 100644 index 9c226f18..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-reference.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface StaticBlock extends Omit { - type: "StaticBlock"; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-value.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-value.d.ts deleted file mode 100644 index 973aa2bb..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type-value.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface BadExpression { - type: undefined; -} - -export interface NewFangledExpression { - type: "NewFangledExpression"; - right: BadExpression; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type.d.ts deleted file mode 100644 index ce4b9830..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/bad-type.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface SomeExpression { - type: "SomeExpression"; - someProperty: any; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-bad.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-bad.d.ts deleted file mode 100644 index 99389eac..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-bad.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface NewFangledExpression { - type: "NewFangledExpression"; - right: BadExpression; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old-order-switched.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old-order-switched.d.ts deleted file mode 100644 index dc0141da..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old-order-switched.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type AssignmentOperator = "="; -interface Pattern { - type: "Pattern" -}; -interface MemberExpression { - type: "MemberExpression" -}; -interface Expression { - type: "Expression" -}; - -export interface AssignmentExpression { - type: "AssignmentExpression"; - operator: AssignmentOperator; - down: Expression; - up: Expression; - left: Pattern | MemberExpression; - right: Expression; - nontraversable: RegExp; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old-other-order.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old-other-order.d.ts deleted file mode 100644 index 3f9d5d34..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old-other-order.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type AssignmentOperator = "="; -interface Pattern { - type: "Pattern" -}; -interface MemberExpression { - type: "MemberExpression" -}; -interface Expression { - type: "Expression" -}; - -export interface AssignmentExpression { - type: "AssignmentExpression"; - operator: AssignmentOperator; - up: Expression; - left: Pattern | MemberExpression; - down: Expression; - right: Expression; - nontraversable: RegExp; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old.d.ts deleted file mode 100644 index 1ac742c0..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys-on-old.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -export type AssignmentOperator = "="; - -interface IgnoreBase { - type: "Line"; -} - -type AnotherIgnore = IgnoreBase; - -interface BasePattern { - type: "Pattern" -}; -interface IgnoreChild extends Omit { -}; - -interface Pattern { - type: "Pattern" -}; -interface MemberExpression { - type: "MemberExpression" -}; -interface Expression { - type: "Expression" -}; - -export interface AssignmentExpression { - type: "AssignmentExpression"; - ignore: IgnoreChild; - anotherIgnore: AnotherIgnore; - operator: AssignmentOperator; - up: Expression; - down: Expression; - left: Pattern | MemberExpression; - right: Expression; - nontraversable: RegExp; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys.d.ts deleted file mode 100644 index 6ff0aa89..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/new-keys.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type AssignmentOperator = "="; -interface Pattern { - type: "Pattern" -}; -interface MemberExpression { - type: "MemberExpression" -}; -interface Expression { - type: "Expression" -}; - -export interface NewFangledExpression { - type: "NewFangledExpression"; - operator: AssignmentOperator; - up: Expression; - down: Expression; - left: Pattern | MemberExpression; - right: Expression; -} diff --git a/packages/eslint-visitor-keys/tests/lib/fixtures/union-omit.d.ts b/packages/eslint-visitor-keys/tests/lib/fixtures/union-omit.d.ts deleted file mode 100644 index d6088df3..00000000 --- a/packages/eslint-visitor-keys/tests/lib/fixtures/union-omit.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface IgnoredStatement { - type: "IgnoredStatement" -} -export interface AnotherStatement { - type: "AnotherStatement"; - anotherToIgnore: IgnoredStatement; -} - -export interface StaticBlock extends Omit { - type: "StaticBlock"; -} diff --git a/packages/espree/README.md b/packages/espree/README.md index b971ea5a..22e3c7e8 100644 --- a/packages/espree/README.md +++ b/packages/espree/README.md @@ -236,6 +236,8 @@ Espree supports all ECMAScript 2024 features and partially supports ECMAScript 2 Because ECMAScript 2025 is still under development, we are implementing features as they are finalized. Currently, Espree supports: * [RegExp Duplicate named capturing groups](https://github.com/tc39/proposal-duplicate-named-capturing-groups) +* [RegExp Pattern modifiers](https://github.com/tc39/proposal-regexp-modifiers) +* [Import Attributes](https://github.com/tc39/proposal-import-attributes) See [finished-proposals.md](https://github.com/tc39/proposals/blob/master/finished-proposals.md) to know what features are finalized. @@ -252,9 +254,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

trunk.io

Silver Sponsors

-

JetBrains Liftoff American Express Workleap

Bronze Sponsors

-

WordHint Anagram Solver Icons8 Discord GitBook Nx HeroCoders

+

Qlty Software trunk.io Shopify

Silver Sponsors

+

Vite JetBrains Liftoff StackBlitz

Bronze Sponsors

+

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

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy