From f887ab51f58c1b3571f9a14832864bc0ca59623f Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 5 Sep 2020 20:20:48 -0700 Subject: [PATCH] feat(scope-manager): add support for JSX scope analysis (#2498) Fixes #2455 And part of #2477 JSX is a first-class citizen of TS, so we should really support it as well. I was going to just rely upon `eslint-plugin-react`'s patch lint rules (`react/jsx-uses-react` and `react/jsx-uses-vars`), but that leaves gaps in our tooling. For example #2455, `consistent-type-imports` makes assumptions and can create invalid fixes for react without this change. We could add options to that lint rule for the factory, but that is kind-of a sub-par experience and future rule authors will likely run into similar problems. - Adds full scope analysis support for JSX. - Adds two new `parserOption`: - `jsxPragma` - the name to use for constructing JSX elements. Defaults to `"React"`. Will be auto detected from the tsconfig. - `jsxFragmentName` - the name that unnamed JSX fragments use. Defaults to `null` (i.e. assumes `React.Fragment`). Will be auto detected from the tsconfig. --- .cspell.json | 2 + .../tests/eslint-rules/no-undef.test.ts | 161 ++++++++++++++++++ .../rules/consistent-type-imports.test.ts | 45 +++++ .../tests/rules/no-unused-vars.test.ts | 99 +++++++++++ packages/parser/README.md | 27 ++- packages/parser/src/parser.ts | 61 +++++-- packages/parser/tests/lib/parser.ts | 40 +++++ packages/scope-manager/README.md | 15 ++ packages/scope-manager/src/analyze.ts | 26 ++- .../scope-manager/src/referencer/Reference.ts | 4 +- .../src/referencer/Referencer.ts | 76 +++++++++ packages/scope-manager/src/scope/ScopeBase.ts | 2 +- packages/scope-manager/tests/fixtures.test.ts | 2 + .../tests/fixtures/jsx/attribute-spread.tsx | 3 + .../fixtures/jsx/attribute-spread.tsx.shot | 65 +++++++ .../tests/fixtures/jsx/attribute.tsx | 4 + .../tests/fixtures/jsx/attribute.tsx.shot | 91 ++++++++++ .../tests/fixtures/jsx/children.tsx | 3 + .../tests/fixtures/jsx/children.tsx.shot | 73 ++++++++ .../fixtures/jsx/component-namespaced.tsx | 6 + .../jsx/component-namespaced.tsx.shot | 103 +++++++++++ .../tests/fixtures/jsx/component.tsx | 3 + .../tests/fixtures/jsx/component.tsx.shot | 66 +++++++ .../jsx/factory/default-jsxFragmentName.tsx | 6 + .../factory/default-jsxFragmentName.tsx.shot | 69 ++++++++ .../factory/default-jsxPragma-fragment.tsx | 5 + .../default-jsxPragma-fragment.tsx.shot | 55 ++++++ .../jsx/factory/default-jsxPragma.tsx | 5 + .../jsx/factory/default-jsxPragma.tsx.shot | 63 +++++++ .../fixtures/jsx/factory/jsxFragmentName.tsx | 7 + .../jsx/factory/jsxFragmentName.tsx.shot | 79 +++++++++ .../jsx/factory/jsxPragma-jsxFragmentName.tsx | 8 + .../jsxPragma-jsxFragmentName.tsx.shot | 93 ++++++++++ .../tests/fixtures/jsx/factory/jsxPragma.tsx | 7 + .../fixtures/jsx/factory/jsxPragma.tsx.shot | 77 +++++++++ .../tests/fixtures/jsx/fragment-children.tsx | 3 + .../fixtures/jsx/fragment-children.tsx.shot | 57 +++++++ .../tests/fixtures/jsx/fragment.tsx | 1 + .../tests/fixtures/jsx/fragment.tsx.shot | 18 ++ .../tests/fixtures/jsx/generic-type-param.tsx | 3 + .../fixtures/jsx/generic-type-param.tsx.shot | 54 ++++++ .../scope-manager/tests/fixtures/jsx/text.tsx | 2 + .../tests/fixtures/jsx/text.tsx.shot | 48 ++++++ packages/types/src/parser-options.ts | 2 + 44 files changed, 1618 insertions(+), 21 deletions(-) create mode 100644 packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/attribute.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/attribute.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/children.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/children.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/component.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/component.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/fragment.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/fragment.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx.shot create mode 100644 packages/scope-manager/tests/fixtures/jsx/text.tsx create mode 100644 packages/scope-manager/tests/fixtures/jsx/text.tsx.shot diff --git a/.cspell.json b/.cspell.json index cbd5138affa..36beb3bb328 100644 --- a/.cspell.json +++ b/.cspell.json @@ -75,6 +75,7 @@ "pluggable", "postprocess", "postprocessor", + "preact", "Premade", "prettier's", "recurse", @@ -88,6 +89,7 @@ "rulesets", "serializers", "superset", + "transpiling", "thenables", "transpiles", "tsconfigs", diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts index c87e5b4433e..7af6497c62a 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts @@ -140,6 +140,65 @@ function predicate(arg: any): asserts arg is T { } } `, + { + code: ` +function Foo() {} +; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: ` +type T = 1; +function Foo() {} + />; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: ` +const x = 1; +function Foo() {} +; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: ` +const x = {}; +function Foo() {} +; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: ` +const x = {}; +function Foo() {} +{x}; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, ], invalid: [ { @@ -175,5 +234,107 @@ function predicate(arg: any): asserts arg is T { }, ], }, + { + code: ';', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + messageId: 'undef', + data: { + name: 'Foo', + }, + line: 1, + column: 2, + }, + ], + }, + { + code: ` +function Foo() {} +; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + messageId: 'undef', + data: { + name: 'x', + }, + line: 3, + column: 12, + }, + ], + }, + { + code: ` +function Foo() {} +; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + messageId: 'undef', + data: { + name: 'x', + }, + line: 3, + column: 10, + }, + ], + }, + { + code: ` +function Foo() {} + />; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + messageId: 'undef', + data: { + name: 'T', + }, + line: 3, + column: 6, + }, + ], + }, + { + code: ` +function Foo() {} +{x}; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + messageId: 'undef', + data: { + name: 'x', + }, + line: 3, + column: 7, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts index 13112793f62..88d6db374a0 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts @@ -180,6 +180,51 @@ ruleTester.run('consistent-type-imports', rule, { `, options: [{ prefer: 'no-type-imports' }], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/2455 + { + code: ` + import React from 'react'; + + export const ComponentFoo: React.FC = () => { + return
Foo Foo
; + }; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: ` + import { h } from 'some-other-jsx-lib'; + + export const ComponentFoo: h.FC = () => { + return
Foo Foo
; + }; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxPragma: 'h', + }, + }, + { + code: ` + import { Fragment } from 'react'; + + export const ComponentFoo: Fragment = () => { + return <>Foo Foo; + }; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxFragmentName: 'Fragment', + }, + }, ], invalid: [ { diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts index bac969579d9..837246f4583 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts @@ -803,6 +803,51 @@ export type Test = U extends (arg: { ? I : never; `, + // https://github.com/typescript-eslint/typescript-eslint/issues/2455 + { + code: ` + import React from 'react'; + + export const ComponentFoo: React.FC = () => { + return
Foo Foo
; + }; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + { + code: ` + import { h } from 'some-other-jsx-lib'; + + export const ComponentFoo: h.FC = () => { + return
Foo Foo
; + }; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxPragma: 'h', + }, + }, + { + code: ` + import { Fragment } from 'react'; + + export const ComponentFoo: Fragment = () => { + return <>Foo Foo; + }; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxFragmentName: 'Fragment', + }, + }, ], invalid: [ @@ -1325,5 +1370,59 @@ type Foo = Array; }, ], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/2455 + { + code: ` +import React from 'react'; +import { Fragment } from 'react'; + +export const ComponentFoo = () => { + return
Foo Foo
; +}; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + errors: [ + { + messageId: 'unusedVar', + line: 3, + data: { + varName: 'Fragment', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: ` +import React from 'react'; +import { h } from 'some-other-jsx-lib'; + +export const ComponentFoo = () => { + return
Foo Foo
; +}; + `, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxPragma: 'h', + }, + errors: [ + { + messageId: 'unusedVar', + line: 2, + data: { + varName: 'React', + action: 'defined', + additional: '', + }, + }, + ], + }, ], }); diff --git a/packages/parser/README.md b/packages/parser/README.md index 5a949425d9f..f6417fd5889 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -54,6 +54,9 @@ interface ParserOptions { globalReturn?: boolean; }; ecmaVersion?: number; + + jsxPragma?: string; + jsxFragmentName?: string | null; lib?: string[]; project?: string | string[]; @@ -98,6 +101,27 @@ Accepts any valid ECMAScript version number: Specifies the version of ECMAScript syntax you want to use. This is used by the parser to determine how to perform scope analysis, and it affects the default +### `parserOptions.jsxPragma` + +Default `'React'` + +The identifier that's used for JSX Elements creation (after transpilation). +If you're using a library other than React (like `preact`), then you should change this value. + +This should not be a member expression - just the root identifier (i.e. use `"React"` instead of `"React.createElement"`). + +If you provide `parserOptions.project`, you do not need to set this, as it will automatically detected from the compiler. + +### `parserOptions.jsxFragmentName` + +Default `null` + +The identifier that's used for JSX fragment elements (after transpilation). +If `null`, assumes transpilation will always use a member of the configured `jsxPragma`. +This should not be a member expression - just the root identifier (i.e. use `"h"` instead of `"h.Fragment"`). + +If you provide `parserOptions.project`, you do not need to set this, as it will automatically detected from the compiler. + ### `parserOptions.lib` Default `['es2018']` @@ -105,7 +129,8 @@ Default `['es2018']` For valid options, see the [TypeScript compiler options](https://www.typescriptlang.org/tsconfig#lib). Specifies the TypeScript `lib`s that are available. This is used by the scope analyser to ensure there are global variables declared for the types exposed by TypeScript. -If you provide `parserOptions.project`, you do not need to set this, as it will automatically detected from the compiler + +If you provide `parserOptions.project`, you do not need to set this, as it will automatically detected from the compiler. ### `parserOptions.project` diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 263779207c9..54dbd399724 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -5,9 +5,13 @@ import { TSESTreeOptions, visitorKeys, } from '@typescript-eslint/typescript-estree'; -import { analyze, ScopeManager } from '@typescript-eslint/scope-manager'; +import { + analyze, + AnalyzeOptions, + ScopeManager, +} from '@typescript-eslint/scope-manager'; import debug from 'debug'; -import { ScriptTarget } from 'typescript'; +import { CompilerOptions, ScriptTarget } from 'typescript'; const log = debug('typescript-eslint:parser:parser'); @@ -33,8 +37,7 @@ function validateBoolean( } const LIB_FILENAME_REGEX = /lib\.(.+)\.d\.ts/; -function getLib(services: ParserServices): Lib[] { - const compilerOptions = services.program.getCompilerOptions(); +function getLib(compilerOptions: CompilerOptions): Lib[] { if (compilerOptions.lib) { return compilerOptions.lib .map(lib => { @@ -100,6 +103,14 @@ function parseForESLint( useJSXTextNode: validateBoolean(options.useJSXTextNode, true), jsx: validateBoolean(options.ecmaFeatures.jsx), }); + const analyzeOptions: AnalyzeOptions = { + ecmaVersion: options.ecmaVersion, + globalReturn: options.ecmaFeatures.globalReturn, + jsxPragma: options.jsxPragma, + jsxFragmentName: options.jsxFragmentName, + lib: options.lib, + sourceType: options.sourceType, + }; if (typeof options.filePath === 'string') { const tsx = options.filePath.endsWith('.tsx'); @@ -123,18 +134,40 @@ function parseForESLint( const { ast, services } = parseAndGenerateServices(code, parserOptions); ast.sourceType = options.sourceType; - // automatically apply the libs configured for the program - if (services.hasFullTypeInformation && options.lib == null) { - options.lib = getLib(services); - log('Resolved libs from program: %o', options.lib); + if (services.hasFullTypeInformation) { + // automatically apply the options configured for the program + const compilerOptions = services.program.getCompilerOptions(); + if (analyzeOptions.lib == null) { + analyzeOptions.lib = getLib(compilerOptions); + log('Resolved libs from program: %o', analyzeOptions.lib); + } + if (parserOptions.jsx === true) { + if ( + analyzeOptions.jsxPragma === undefined && + compilerOptions.jsxFactory != null + ) { + // in case the user has specified something like "preact.h" + const factory = compilerOptions.jsxFactory.split('.')[0].trim(); + analyzeOptions.jsxPragma = factory; + log('Resolved jsxPragma from program: %s', analyzeOptions.jsxPragma); + } + if ( + analyzeOptions.jsxFragmentName === undefined && + compilerOptions.jsxFragmentFactory != null + ) { + // in case the user has specified something like "preact.Fragment" + const fragFactory = compilerOptions.jsxFragmentFactory + .split('.')[0] + .trim(); + analyzeOptions.jsxFragmentName = fragFactory; + log( + 'Resolved jsxFragmentName from program: %s', + analyzeOptions.jsxFragmentName, + ); + } + } } - const analyzeOptions = { - ecmaVersion: options.ecmaVersion, - globalReturn: options.ecmaFeatures.globalReturn, - lib: options.lib, - sourceType: options.sourceType, - }; const scopeManager = analyze(ast, analyzeOptions); return { ast, services, scopeManager, visitorKeys }; diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index ba4a1663c8e..41e22a911f9 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -1,5 +1,6 @@ import { TSESLint } from '@typescript-eslint/experimental-utils'; import * as typescriptESTree from '@typescript-eslint/typescript-estree/dist/parser'; +import * as scopeManager from '@typescript-eslint/scope-manager/dist/analyze'; import { parse, parseForESLint } from '../../src/parser'; describe('parser', () => { @@ -70,4 +71,43 @@ describe('parser', () => { warnOnUnsupportedTypeScriptVersion: false, }); }); + + it('analyze() should be called with options', () => { + const code = 'const valid = true;'; + const spy = jest.spyOn(scopeManager, 'analyze'); + const config: TSESLint.ParserOptions = { + loc: false, + comment: false, + range: false, + tokens: false, + sourceType: 'module' as const, + ecmaVersion: 2018, + ecmaFeatures: { + globalReturn: false, + jsx: false, + }, + // scope-manager specific + lib: ['dom.iterable'], + jsxPragma: 'Foo', + jsxFragmentName: 'Bar', + // ts-estree specific + filePath: 'isolated-file.src.ts', + project: 'tsconfig.json', + useJSXTextNode: false, + errorOnUnknownASTType: false, + errorOnTypeScriptSyntacticAndSemanticIssues: false, + tsconfigRootDir: 'tests/fixtures/services', + extraFileExtensions: ['.foo'], + }; + parseForESLint(code, config); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenLastCalledWith(expect.anything(), { + ecmaVersion: 2018, + globalReturn: false, + lib: ['dom.iterable'], + jsxPragma: 'Foo', + jsxFragmentName: 'Bar', + sourceType: 'module', + }); + }); }); diff --git a/packages/scope-manager/README.md b/packages/scope-manager/README.md index 6d389a32f31..24ccd839dff 100644 --- a/packages/scope-manager/README.md +++ b/packages/scope-manager/README.md @@ -55,6 +55,21 @@ interface AnalyzeOptions { */ impliedStrict?: boolean; + /** + * The identifier that's used for JSX Element creation (after transpilation). + * This should not be a member expression - just the root identifier (i.e. use "React" instead of "React.createElement"). + * Defaults to `"React"`. + */ + jsxPragma?: string; + + /** + * The identifier that's used for JSX fragment elements (after transpilation). + * If `null`, assumes transpilation will always use a member on `jsxFactory` (i.e. React.Fragment). + * This should not be a member expression - just the root identifier (i.e. use "h" instead of "h.Fragment"). + * Defaults to `null`. + */ + jsxFragmentName?: string | null; + /** * The lib used by the project. * This automatically defines a type variable for any types provided by the configured TS libs. diff --git a/packages/scope-manager/src/analyze.ts b/packages/scope-manager/src/analyze.ts index ee3ac77de96..9e734925da1 100644 --- a/packages/scope-manager/src/analyze.ts +++ b/packages/scope-manager/src/analyze.ts @@ -33,6 +33,21 @@ interface AnalyzeOptions { */ impliedStrict?: boolean; + /** + * The identifier that's used for JSX Element creation (after transpilation). + * This should not be a member expression - just the root identifier (i.e. use "React" instead of "React.createElement"). + * Defaults to `"React"`. + */ + jsxPragma?: string; + + /** + * The identifier that's used for JSX fragment elements (after transpilation). + * If `null`, assumes transpilation will always use a member on `jsxFactory` (i.e. React.Fragment). + * This should not be a member expression - just the root identifier (i.e. use "h" instead of "h.Fragment"). + * Defaults to `null`. + */ + jsxFragmentName?: string | null; + /** * The lib used by the project. * This automatically defines a type variable for any types provided by the configured TS libs. @@ -53,6 +68,8 @@ const DEFAULT_OPTIONS: Required = { ecmaVersion: 2018, globalReturn: false, impliedStrict: false, + jsxPragma: 'React', + jsxFragmentName: null, lib: ['es2018'], sourceType: 'script', }; @@ -78,13 +95,16 @@ function analyze( const ecmaVersion = providedOptions?.ecmaVersion ?? DEFAULT_OPTIONS.ecmaVersion; const options: Required = { + childVisitorKeys: + providedOptions?.childVisitorKeys ?? DEFAULT_OPTIONS.childVisitorKeys, + ecmaVersion, globalReturn: providedOptions?.globalReturn ?? DEFAULT_OPTIONS.globalReturn, impliedStrict: providedOptions?.impliedStrict ?? DEFAULT_OPTIONS.impliedStrict, + jsxPragma: providedOptions?.jsxPragma ?? DEFAULT_OPTIONS.jsxPragma, + jsxFragmentName: + providedOptions?.jsxFragmentName ?? DEFAULT_OPTIONS.jsxFragmentName, sourceType: providedOptions?.sourceType ?? DEFAULT_OPTIONS.sourceType, - ecmaVersion, - childVisitorKeys: - providedOptions?.childVisitorKeys ?? DEFAULT_OPTIONS.childVisitorKeys, lib: providedOptions?.lib ?? [mapEcmaVersion(ecmaVersion)], }; diff --git a/packages/scope-manager/src/referencer/Reference.ts b/packages/scope-manager/src/referencer/Reference.ts index 5786c501730..9cf624f82a1 100644 --- a/packages/scope-manager/src/referencer/Reference.ts +++ b/packages/scope-manager/src/referencer/Reference.ts @@ -43,7 +43,7 @@ class Reference { * Identifier syntax node. * @public */ - public readonly identifier: TSESTree.Identifier; + public readonly identifier: TSESTree.Identifier | TSESTree.JSXIdentifier; /** * `true` if this writing reference is a variable initializer or a default value. * @public @@ -82,7 +82,7 @@ class Reference { } constructor( - identifier: TSESTree.Identifier, + identifier: TSESTree.Identifier | TSESTree.JSXIdentifier, scope: Scope, flag: ReferenceFlag, writeExpr?: TSESTree.Node | null, diff --git a/packages/scope-manager/src/referencer/Referencer.ts b/packages/scope-manager/src/referencer/Referencer.ts index c0c49ef9728..bd88b6a5996 100644 --- a/packages/scope-manager/src/referencer/Referencer.ts +++ b/packages/scope-manager/src/referencer/Referencer.ts @@ -22,18 +22,26 @@ import { lib as TSLibraries } from '../lib'; import { Scope, GlobalScope } from '../scope'; interface ReferencerOptions extends VisitorOptions { + jsxPragma: string; + jsxFragmentName: string | null; lib: Lib[]; } // Referencing variables and creating bindings. class Referencer extends Visitor { #isInnerMethodDefinition: boolean; + #jsxPragma: string; + #jsxFragmentName: string | null; + #hasReferencedJsxFactory = false; + #hasReferencedJsxFragmentFactory = false; #lib: Lib[]; public readonly scopeManager: ScopeManager; constructor(options: ReferencerOptions, scopeManager: ScopeManager) { super(options); this.scopeManager = scopeManager; + this.#jsxPragma = options.jsxPragma; + this.#jsxFragmentName = options.jsxFragmentName; this.#lib = options.lib; this.#isInnerMethodDefinition = false; } @@ -99,6 +107,46 @@ class Referencer extends Visitor { } } + /** + * Searches for a variable named "name" in the upper scopes and adds a pseudo-reference from itself to itself + */ + private referenceInSomeUpperScope(name: string): boolean { + let scope = this.scopeManager.currentScope; + while (scope) { + const variable = scope.set.get(name); + if (!variable) { + scope = scope.upper; + continue; + } + + scope.referenceValue(variable.identifiers[0]); + return true; + } + + return false; + } + + private referenceJsxPragma(): void { + if (this.#hasReferencedJsxFactory) { + return; + } + this.#hasReferencedJsxFactory = this.referenceInSomeUpperScope( + this.#jsxPragma, + ); + } + + private referenceJsxFragment(): void { + if ( + this.#jsxFragmentName === null || + this.#hasReferencedJsxFragmentFactory + ) { + return; + } + this.#hasReferencedJsxFragmentFactory = this.referenceInSomeUpperScope( + this.#jsxFragmentName, + ); + } + /////////////////// // Visit helpers // /////////////////// @@ -498,6 +546,34 @@ class Referencer extends Visitor { ImportVisitor.visit(this, node); } + protected JSXAttribute(node: TSESTree.JSXAttribute): void { + this.visit(node.value); + } + + protected JSXFragment(node: TSESTree.JSXFragment): void { + this.referenceJsxPragma(); + this.referenceJsxFragment(); + this.visitChildren(node); + } + + protected JSXIdentifier(node: TSESTree.JSXIdentifier): void { + this.currentScope().referenceValue(node); + } + + protected JSXMemberExpression(node: TSESTree.JSXMemberExpression): void { + this.visit(node.object); + // we don't ever reference the property as it's always going to be a property on the thing + } + + protected JSXOpeningElement(node: TSESTree.JSXOpeningElement): void { + this.referenceJsxPragma(); + this.visit(node.name); + this.visitType(node.typeParameters); + for (const attr of node.attributes) { + this.visit(attr); + } + } + protected LabeledStatement(node: TSESTree.LabeledStatement): void { this.visit(node.body); } diff --git a/packages/scope-manager/src/scope/ScopeBase.ts b/packages/scope-manager/src/scope/ScopeBase.ts index cc38cdb3f97..9b852dcff6a 100644 --- a/packages/scope-manager/src/scope/ScopeBase.ts +++ b/packages/scope-manager/src/scope/ScopeBase.ts @@ -446,7 +446,7 @@ abstract class ScopeBase< } public referenceValue( - node: TSESTree.Identifier, + node: TSESTree.Identifier | TSESTree.JSXIdentifier, assign: ReferenceFlag = ReferenceFlag.Read, writeExpr?: TSESTree.Expression | null, maybeImplicitGlobal?: ReferenceImplicitGlobal | null, diff --git a/packages/scope-manager/tests/fixtures.test.ts b/packages/scope-manager/tests/fixtures.test.ts index 24bd0fd80f0..08eea4f4995 100644 --- a/packages/scope-manager/tests/fixtures.test.ts +++ b/packages/scope-manager/tests/fixtures.test.ts @@ -41,6 +41,8 @@ const ALLOWED_OPTIONS: Map = new Map< ['ecmaVersion', ['number']], ['globalReturn', ['boolean']], ['impliedStrict', ['boolean']], + ['jsxPragma', ['string']], + ['jsxFragmentName', ['string']], ['sourceType', ['string', new Set(['module', 'script'])]], ]); diff --git a/packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx b/packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx new file mode 100644 index 00000000000..b501cbc9b17 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx @@ -0,0 +1,3 @@ +const x = {}; + +; diff --git a/packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx.shot new file mode 100644 index 00000000000..fd34f473d78 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/attribute-spread.tsx.shot @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx attribute-spread 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$1, + writeExpr: ObjectExpression$2, + }, + Reference$3 { + identifier: Identifier<"x">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [ + Reference$1, + Reference$2 { + identifier: JSXIdentifier$4, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: null, + }, + Reference$3, + ], + set: Map { + "x" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/attribute.tsx b/packages/scope-manager/tests/fixtures/jsx/attribute.tsx new file mode 100644 index 00000000000..e915453933d --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/attribute.tsx @@ -0,0 +1,4 @@ +const x = 1; +const attr = 2; // should be unreferenced + +; diff --git a/packages/scope-manager/tests/fixtures/jsx/attribute.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/attribute.tsx.shot new file mode 100644 index 00000000000..a8764786318 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/attribute.tsx.shot @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx attribute 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$1, + writeExpr: Literal$2, + }, + Reference$4 { + identifier: Identifier<"x">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$2 { + defs: Array [ + VariableDefinition$2 { + name: Identifier<"attr">, + node: VariableDeclarator$3, + }, + ], + name: "attr", + references: Array [ + Reference$2 { + identifier: Identifier<"attr">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$4, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$5, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + Reference$3 { + identifier: JSXIdentifier$6, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: null, + }, + Reference$4, + ], + set: Map { + "x" => Variable$1, + "attr" => Variable$2, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/children.tsx b/packages/scope-manager/tests/fixtures/jsx/children.tsx new file mode 100644 index 00000000000..eea4145f68d --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/children.tsx @@ -0,0 +1,3 @@ +const child = 1; + +{child}; diff --git a/packages/scope-manager/tests/fixtures/jsx/children.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/children.tsx.shot new file mode 100644 index 00000000000..9fc1dfb4953 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/children.tsx.shot @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx children 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"child">, + node: VariableDeclarator$1, + }, + ], + name: "child", + references: Array [ + Reference$1 { + identifier: Identifier<"child">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$1, + writeExpr: Literal$2, + }, + Reference$3 { + identifier: Identifier<"child">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [ + Reference$1, + Reference$2 { + identifier: JSXIdentifier$4, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: null, + }, + Reference$3, + Reference$4 { + identifier: JSXIdentifier$5, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: null, + }, + ], + set: Map { + "child" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx b/packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx new file mode 100644 index 00000000000..fa0a0f91521 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx @@ -0,0 +1,6 @@ +const X = { + Foo() {}, +}; +const Foo = 1; // should be unreferenced + +; diff --git a/packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx.shot new file mode 100644 index 00000000000..c5bc7eb9d4e --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/component-namespaced.tsx.shot @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx component-namespaced 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"X">, + node: VariableDeclarator$1, + }, + ], + name: "X", + references: Array [ + Reference$1 { + identifier: Identifier<"X">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$1, + writeExpr: ObjectExpression$2, + }, + Reference$3 { + identifier: JSXIdentifier$3, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$2 { + defs: Array [], + name: "arguments", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$3 { + defs: Array [ + VariableDefinition$2 { + name: Identifier<"Foo">, + node: VariableDeclarator$4, + }, + ], + name: "Foo", + references: Array [ + Reference$2 { + identifier: Identifier<"Foo">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$3, + writeExpr: Literal$5, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$6, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + Reference$3, + ], + set: Map { + "X" => Variable$1, + "Foo" => Variable$3, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + Variable$3, + ], + }, + FunctionScope$2 { + block: FunctionExpression$7, + isStrict: false, + references: Array [], + set: Map { + "arguments" => Variable$2, + }, + type: "function", + upper: GlobalScope$1, + variables: Array [ + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/component.tsx b/packages/scope-manager/tests/fixtures/jsx/component.tsx new file mode 100644 index 00000000000..20f898b540c --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/component.tsx @@ -0,0 +1,3 @@ +function Foo() {} + +; diff --git a/packages/scope-manager/tests/fixtures/jsx/component.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/component.tsx.shot new file mode 100644 index 00000000000..a37331e7f7e --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/component.tsx.shot @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx component 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + FunctionNameDefinition$1 { + name: Identifier<"Foo">, + node: FunctionDeclaration$1, + }, + ], + name: "Foo", + references: Array [ + Reference$1 { + identifier: JSXIdentifier$2, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$2 { + defs: Array [], + name: "arguments", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [ + Reference$1, + ], + set: Map { + "Foo" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + FunctionScope$2 { + block: FunctionDeclaration$1, + isStrict: false, + references: Array [], + set: Map { + "arguments" => Variable$2, + }, + type: "function", + upper: GlobalScope$1, + variables: Array [ + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx new file mode 100644 index 00000000000..de94f8168c8 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx @@ -0,0 +1,6 @@ +//// @sourceType = 'module' + +import React from 'react'; +import { Fragment } from 'react'; // should be unreferenced + +<>; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx.shot new file mode 100644 index 00000000000..76e5b24ecc8 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxFragmentName.tsx.shot @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx factory default-jsxFragmentName 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + ImportBindingDefinition$1 { + name: Identifier<"React">, + node: ImportDefaultSpecifier$1, + }, + ], + name: "React", + references: Array [ + Reference$1 { + identifier: Identifier<"React">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$2 { + defs: Array [ + ImportBindingDefinition$2 { + name: Identifier<"Fragment">, + node: ImportSpecifier$2, + }, + ], + name: "Fragment", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [], + set: Map {}, + type: "global", + upper: null, + variables: Array [], + }, + ModuleScope$2 { + block: Program$3, + isStrict: true, + references: Array [ + Reference$1, + ], + set: Map { + "React" => Variable$1, + "Fragment" => Variable$2, + }, + type: "module", + upper: GlobalScope$1, + variables: Array [ + Variable$1, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx new file mode 100644 index 00000000000..8fce679e746 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx @@ -0,0 +1,5 @@ +//// @sourceType = 'module' + +import React from 'react'; + +<>; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx.shot new file mode 100644 index 00000000000..4b7ea0925e1 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma-fragment.tsx.shot @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx factory default-jsxPragma-fragment 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + ImportBindingDefinition$1 { + name: Identifier<"React">, + node: ImportDefaultSpecifier$1, + }, + ], + name: "React", + references: Array [ + Reference$1 { + identifier: Identifier<"React">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$2, + isStrict: false, + references: Array [], + set: Map {}, + type: "global", + upper: null, + variables: Array [], + }, + ModuleScope$2 { + block: Program$2, + isStrict: true, + references: Array [ + Reference$1, + ], + set: Map { + "React" => Variable$1, + }, + type: "module", + upper: GlobalScope$1, + variables: Array [ + Variable$1, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx new file mode 100644 index 00000000000..bf3f5088885 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx @@ -0,0 +1,5 @@ +//// @sourceType = 'module' + +import React from 'react'; + +; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx.shot new file mode 100644 index 00000000000..4c7c51c772e --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/default-jsxPragma.tsx.shot @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx factory default-jsxPragma 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + ImportBindingDefinition$1 { + name: Identifier<"React">, + node: ImportDefaultSpecifier$1, + }, + ], + name: "React", + references: Array [ + Reference$1 { + identifier: Identifier<"React">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$2, + isStrict: false, + references: Array [], + set: Map {}, + type: "global", + upper: null, + variables: Array [], + }, + ModuleScope$2 { + block: Program$2, + isStrict: true, + references: Array [ + Reference$1, + Reference$2 { + identifier: JSXIdentifier$3, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: null, + }, + ], + set: Map { + "React" => Variable$1, + }, + type: "module", + upper: GlobalScope$1, + variables: Array [ + Variable$1, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx b/packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx new file mode 100644 index 00000000000..22df1a693d4 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx @@ -0,0 +1,7 @@ +//// @sourceType = 'module' +//// @jsxFragmentName = 'Fragment' + +import React from 'react'; // should be unreferenced +import { Fragment } from 'preact'; + +<>; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx.shot new file mode 100644 index 00000000000..8392610d9a6 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/jsxFragmentName.tsx.shot @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx factory jsxFragmentName 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + ImportBindingDefinition$1 { + name: Identifier<"React">, + node: ImportDefaultSpecifier$1, + }, + ], + name: "React", + references: Array [ + Reference$1 { + identifier: Identifier<"React">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$2 { + defs: Array [ + ImportBindingDefinition$2 { + name: Identifier<"Fragment">, + node: ImportSpecifier$2, + }, + ], + name: "Fragment", + references: Array [ + Reference$2 { + identifier: Identifier<"Fragment">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [], + set: Map {}, + type: "global", + upper: null, + variables: Array [], + }, + ModuleScope$2 { + block: Program$3, + isStrict: true, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "React" => Variable$1, + "Fragment" => Variable$2, + }, + type: "module", + upper: GlobalScope$1, + variables: Array [ + Variable$1, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx new file mode 100644 index 00000000000..e4bd844170b --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx @@ -0,0 +1,8 @@ +//// @sourceType = 'module' +//// @jsxPragma = 'h' +//// @jsxFragmentName = 'Fragment' + +import React from 'react'; // should be unreferenced +import { h, Fragment } from 'preact'; + +<>; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx.shot new file mode 100644 index 00000000000..5dc43c1d89f --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma-jsxFragmentName.tsx.shot @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx factory jsxPragma-jsxFragmentName 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + ImportBindingDefinition$1 { + name: Identifier<"React">, + node: ImportDefaultSpecifier$1, + }, + ], + name: "React", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$2 { + defs: Array [ + ImportBindingDefinition$2 { + name: Identifier<"h">, + node: ImportSpecifier$2, + }, + ], + name: "h", + references: Array [ + Reference$1 { + identifier: Identifier<"h">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$3 { + defs: Array [ + ImportBindingDefinition$3 { + name: Identifier<"Fragment">, + node: ImportSpecifier$3, + }, + ], + name: "Fragment", + references: Array [ + Reference$2 { + identifier: Identifier<"Fragment">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$3, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [], + set: Map {}, + type: "global", + upper: null, + variables: Array [], + }, + ModuleScope$2 { + block: Program$4, + isStrict: true, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "React" => Variable$1, + "h" => Variable$2, + "Fragment" => Variable$3, + }, + type: "module", + upper: GlobalScope$1, + variables: Array [ + Variable$1, + Variable$2, + Variable$3, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx new file mode 100644 index 00000000000..61a64b504b9 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx @@ -0,0 +1,7 @@ +//// @sourceType = 'module' +//// @jsxPragma = 'h' + +import React from 'react'; // should be unreferenced +import { h } from 'preact'; + +; diff --git a/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx.shot new file mode 100644 index 00000000000..1fb48ba9b05 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/factory/jsxPragma.tsx.shot @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx factory jsxPragma 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + ImportBindingDefinition$1 { + name: Identifier<"React">, + node: ImportDefaultSpecifier$1, + }, + ], + name: "React", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$2 { + defs: Array [ + ImportBindingDefinition$2 { + name: Identifier<"h">, + node: ImportSpecifier$2, + }, + ], + name: "h", + references: Array [ + Reference$1 { + identifier: Identifier<"h">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: true, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [], + set: Map {}, + type: "global", + upper: null, + variables: Array [], + }, + ModuleScope$2 { + block: Program$3, + isStrict: true, + references: Array [ + Reference$1, + Reference$2 { + identifier: JSXIdentifier$4, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: null, + }, + ], + set: Map { + "React" => Variable$1, + "h" => Variable$2, + }, + type: "module", + upper: GlobalScope$1, + variables: Array [ + Variable$1, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx b/packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx new file mode 100644 index 00000000000..eb4c293d13b --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx @@ -0,0 +1,3 @@ +const child = 1; + +<>{child}; diff --git a/packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx.shot new file mode 100644 index 00000000000..ada53e07e79 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/fragment-children.tsx.shot @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx fragment-children 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"child">, + node: VariableDeclarator$1, + }, + ], + name: "child", + references: Array [ + Reference$1 { + identifier: Identifier<"child">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$1, + writeExpr: Literal$2, + }, + Reference$2 { + identifier: Identifier<"child">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "child" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/fragment.tsx b/packages/scope-manager/tests/fixtures/jsx/fragment.tsx new file mode 100644 index 00000000000..ef5e491259c --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/fragment.tsx @@ -0,0 +1 @@ +<>; diff --git a/packages/scope-manager/tests/fixtures/jsx/fragment.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/fragment.tsx.shot new file mode 100644 index 00000000000..7bf20feb1aa --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/fragment.tsx.shot @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx fragment 1`] = ` +ScopeManager { + variables: Array [], + scopes: Array [ + GlobalScope$1 { + block: Program$1, + isStrict: false, + references: Array [], + set: Map {}, + type: "global", + upper: null, + variables: Array [], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx b/packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx new file mode 100644 index 00000000000..af9a714e5ac --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx @@ -0,0 +1,3 @@ +type T = 1; + + />; diff --git a/packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx.shot new file mode 100644 index 00000000000..1ea1b8dbca3 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/generic-type-param.tsx.shot @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx generic-type-param 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + TypeDefinition$1 { + name: Identifier<"T">, + node: TSTypeAliasDeclaration$1, + }, + ], + name: "T", + references: Array [ + Reference$2 { + identifier: Identifier<"T">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$1, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$2, + isStrict: false, + references: Array [ + Reference$1 { + identifier: JSXIdentifier$3, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: null, + }, + Reference$2, + ], + set: Map { + "T" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/jsx/text.tsx b/packages/scope-manager/tests/fixtures/jsx/text.tsx new file mode 100644 index 00000000000..a5bb8d61130 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/text.tsx @@ -0,0 +1,2 @@ +const Foo = 1; // should be unreferenced +<>Foo; diff --git a/packages/scope-manager/tests/fixtures/jsx/text.tsx.shot b/packages/scope-manager/tests/fixtures/jsx/text.tsx.shot new file mode 100644 index 00000000000..b901d67292f --- /dev/null +++ b/packages/scope-manager/tests/fixtures/jsx/text.tsx.shot @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsx text 1`] = ` +ScopeManager { + variables: Array [ + Variable$1 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"Foo">, + node: VariableDeclarator$1, + }, + ], + name: "Foo", + references: Array [ + Reference$1 { + identifier: Identifier<"Foo">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$1, + writeExpr: Literal$2, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$3, + isStrict: false, + references: Array [ + Reference$1, + ], + set: Map { + "Foo" => Variable$1, + }, + type: "global", + upper: null, + variables: Array [ + Variable$1, + ], + }, + ], +} +`; diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index c9623688e16..898f6583ef6 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -28,6 +28,8 @@ interface ParserOptions { ecmaVersion?: EcmaVersion; // scope-manager specific + jsxPragma?: string; + jsxFragmentName?: string | null; lib?: Lib[]; // typescript-estree specific