From ddd7b4c991f5417cd06f3484cb2ac01e6b884659 Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Tue, 1 Apr 2025 04:35:53 +0800 Subject: [PATCH] feat(react-x): add 'jsx-uses-react' rule --- apps/website/content/docs/rules/meta.json | 1 + apps/website/content/docs/rules/overview.mdx | 1 + .../src/configs/recommended-typescript.ts | 1 + .../src/configs/recommended.ts | 1 + .../eslint-plugin-react-x/src/plugin.ts | 2 + .../src/rules/jsx-uses-react.md | 65 +++++++++++++++++++ .../src/rules/jsx-uses-react.spec.ts | 27 ++++++++ .../src/rules/jsx-uses-react.ts | 48 ++++++++++++++ .../src/rules/jsx-uses-vars.spec.ts | 5 +- .../src/rules/jsx-uses-vars.ts | 2 +- .../plugins/eslint-plugin/src/configs/all.ts | 1 + .../plugins/eslint-plugin/src/configs/core.ts | 1 + .../src/configs/recommended-typescript.ts | 1 + packages/utilities/kit/docs/README.md | 2 + .../utilities/kit/docs/functions/toRegExp.md | 4 ++ .../kit/docs/variables/RE_JSX_ANNOTATION.md | 11 ++++ .../kit/docs/variables/RE_JS_IDENTIFIER.md | 11 ++++ packages/utilities/kit/src/RegExp.ts | 12 +++- 18 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts create mode 100644 packages/utilities/kit/docs/variables/RE_JSX_ANNOTATION.md create mode 100644 packages/utilities/kit/docs/variables/RE_JS_IDENTIFIER.md diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 4dfa27f3dd..a0ca3b1833 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -4,6 +4,7 @@ "---Core Rules---", "jsx-no-duplicate-props", "jsx-no-undef", + "jsx-uses-react", "jsx-uses-vars", "no-access-state-in-setstate", "no-array-index-key", diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index 938fe07812..52b4bc1735 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -27,6 +27,7 @@ Linter rules can have false positives, false negatives, and some rules are depen | :----------------------------------------------------------------------------------- | :- | :------: | :---------------------------------------------------------------------------------------------------- | :-----: | | [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ | | Disallow duplicate props in JSX elements | | | [`jsx-no-undef`](./jsx-no-undef) | 0️⃣ | | Disallow undefined variables in JSX elements | | +| [`jsx-uses-react`](./jsx-uses-react) | 1️⃣ | | Marks React variables as used when JSX is used in the file | | | [`jsx-uses-vars`](./jsx-uses-vars) | 1️⃣ | | Marks variables used in JSX elements as used | | | [`no-access-state-in-setstate`](./no-access-state-in-setstate) | 2️⃣ | | Disallow accessing `this.state` inside `setState` calls | | | [`no-array-index-key`](./no-array-index-key) | 1️⃣ | | Disallow an item's index in the array as its key | | diff --git a/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts b/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts index 8bf3053930..1ef3916d21 100644 --- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts +++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts @@ -8,6 +8,7 @@ export const rules = { ...recommended.rules, "react-x/jsx-no-duplicate-props": "off", "react-x/jsx-no-undef": "off", + "react-x/jsx-uses-react": "off", "react-x/jsx-uses-vars": "off", } as const satisfies RulePreset; diff --git a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts index e1c5e45ff5..b2eb35293e 100644 --- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts +++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts @@ -5,6 +5,7 @@ export const name = "react-x/recommended"; export const rules = { "react-x/jsx-no-duplicate-props": "warn", + "react-x/jsx-uses-react": "warn", "react-x/jsx-uses-vars": "warn", "react-x/no-access-state-in-setstate": "error", "react-x/no-array-index-key": "warn", diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index 4e485b401e..a5048af65a 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -3,6 +3,7 @@ import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean"; import avoidShorthandFragment from "./rules/avoid-shorthand-fragment"; import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props"; import jsxNoUndef from "./rules/jsx-no-undef"; +import jsxUsesReact from "./rules/jsx-uses-react"; import jsxUsesVars from "./rules/jsx-uses-vars"; import noAccessStateInSetstate from "./rules/no-access-state-in-setstate"; import noArrayIndexKey from "./rules/no-array-index-key"; @@ -113,6 +114,7 @@ export const plugin = { // Part: JSX only rules "jsx-no-duplicate-props": jsxNoDuplicateProps, "jsx-no-undef": jsxNoUndef, + "jsx-uses-react": jsxUsesReact, "jsx-uses-vars": jsxUsesVars, // Part: deprecated rules diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.md b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.md new file mode 100644 index 0000000000..51f5171a80 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.md @@ -0,0 +1,65 @@ +--- +title: jsx-uses-react +--- + +**Full Name in `eslint-plugin-react-x`** + +```plain copy +react-x/jsx-uses-react +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```plain copy +@eslint-react/jsx-uses-react +``` + +**Presets** + +- `core` +- `recommended` + +## Description + +Marks React variables as used when JSX is used in the file. + +If you are using the `@jsx` pragma this rule will mark the designated variable and not the React one. + +This rule does nothing when using the [New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) or if the `no-unused-vars` rule is not enabled. + +## Examples + +### Failing + +```tsx +import React from "react"; +// nothing to do with React +``` + +```tsx +/** @jsx Foo */ +import React from "react"; +// nothing to do with React + +const Hello =
Hello
; +``` + +### Passing + +```tsx +import React from "react"; + +const Hello =
Hello
; +``` + +```tsx +/** @jsx Foo */ +import Foo from "foo"; + +const Hello =
Hello
; +``` + +## Implementation + +- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts) +- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts) diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts new file mode 100644 index 0000000000..c67151c987 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts @@ -0,0 +1,27 @@ +import tsx from "dedent"; + +import { allValid, ruleTester } from "../../../../../test"; +import rule from "./jsx-uses-react"; + +ruleTester.run("no-unused-vars", rule, { + // TODO: Add invalid test cases + invalid: [], + valid: [ + ...allValid, + { + code: tsx` + import React from "react"; + + const Hello =
Hello
; + `, + }, + { + code: tsx` + /** @jsx Foo */ + import Foo from "foo"; + + const Hello =
Hello
; + `, + }, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts new file mode 100644 index 0000000000..266e5869ab --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts @@ -0,0 +1,48 @@ +import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { CamelCase } from "string-ts"; +import { JsxRuntime, type RuleContext, type RuleFeature } from "@eslint-react/kit"; + +import { JsxEmit } from "typescript"; +import { createRule } from "../utils"; + +export const RULE_NAME = "jsx-uses-react"; + +export const RULE_FEATURES = [] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "Marks React variables as used when JSX is used in the file.", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + messages: { + jsxUsesReact: "", + }, + schema: [], + }, + name: RULE_NAME, + create, + defaultOptions: [], +}); + +export function create(context: RuleContext): RuleListener { + const { jsx, jsxFactory, jsxFragmentFactory, reactNamespace } = JsxRuntime.getJsxRuntimeOptionsFromContext(context); + // If we are using the New JSX Transform, this rule should do nothing. + if (jsx === JsxEmit.ReactJSX || jsx === JsxEmit.ReactJSXDev) return {}; + return { + JSXFragment(node) { + context.sourceCode.markVariableAsUsed(jsxFragmentFactory, node); + }, + JSXOpeningElement(node) { + context.sourceCode.markVariableAsUsed(reactNamespace, node); + context.sourceCode.markVariableAsUsed(jsxFactory, node); + }, + JSXOpeningFragment(node) { + context.sourceCode.markVariableAsUsed(reactNamespace, node); + context.sourceCode.markVariableAsUsed(jsxFactory, node); + }, + }; +} diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.spec.ts index dc7d38eb08..4ff519e2d5 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.spec.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.spec.ts @@ -1,9 +1,10 @@ import tsx from "dedent"; import { allValid, ruleTester } from "../../../../../test"; -import rule, { RULE_NAME } from "./jsx-uses-vars"; +import rule from "./jsx-uses-vars"; -ruleTester.run(RULE_NAME, rule, { +ruleTester.run("no-unused-vars", rule, { + // TODO: Add invalid test cases invalid: [], valid: [ ...allValid, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.ts index e1f32a242c..2c2a392cd2 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-vars.ts @@ -19,7 +19,7 @@ export default createRule<[], MessageID>({ [Symbol.for("rule_features")]: RULE_FEATURES, }, messages: { - jsxUsesVars: "JSX variables should be marked as used.", + jsxUsesVars: "", }, schema: [], }, diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts index 0d1bcb6cb9..5b9da54f77 100644 --- a/packages/plugins/eslint-plugin/src/configs/all.ts +++ b/packages/plugins/eslint-plugin/src/configs/all.ts @@ -14,6 +14,7 @@ export const rules = { "@eslint-react/avoid-shorthand-fragment": "warn", "@eslint-react/jsx-no-duplicate-props": "warn", "@eslint-react/jsx-no-undef": "error", + "@eslint-react/jsx-uses-react": "warn", "@eslint-react/jsx-uses-vars": "warn", "@eslint-react/no-access-state-in-setstate": "error", "@eslint-react/no-array-index-key": "warn", diff --git a/packages/plugins/eslint-plugin/src/configs/core.ts b/packages/plugins/eslint-plugin/src/configs/core.ts index 5cdd5bf528..df36e90865 100644 --- a/packages/plugins/eslint-plugin/src/configs/core.ts +++ b/packages/plugins/eslint-plugin/src/configs/core.ts @@ -6,6 +6,7 @@ export const name = "@eslint-react/core"; export const rules = { "@eslint-react/jsx-no-duplicate-props": "warn", + "@eslint-react/jsx-uses-react": "warn", "@eslint-react/jsx-uses-vars": "warn", "@eslint-react/no-access-state-in-setstate": "error", "@eslint-react/no-array-index-key": "warn", diff --git a/packages/plugins/eslint-plugin/src/configs/recommended-typescript.ts b/packages/plugins/eslint-plugin/src/configs/recommended-typescript.ts index 0bbd2ed475..9f2d96634c 100644 --- a/packages/plugins/eslint-plugin/src/configs/recommended-typescript.ts +++ b/packages/plugins/eslint-plugin/src/configs/recommended-typescript.ts @@ -8,6 +8,7 @@ export const rules = { ...recommended.rules, "@eslint-react/dom/no-unknown-property": "off", "@eslint-react/jsx-no-duplicate-props": "off", + "@eslint-react/jsx-uses-react": "off", "@eslint-react/jsx-uses-vars": "off", } as const satisfies RulePreset; diff --git a/packages/utilities/kit/docs/README.md b/packages/utilities/kit/docs/README.md index 34fbacffae..97d241ea8c 100644 --- a/packages/utilities/kit/docs/README.md +++ b/packages/utilities/kit/docs/README.md @@ -25,6 +25,8 @@ - [RE\_CONSTANT\_CASE](variables/RE_CONSTANT_CASE.md) - [RE\_JAVASCRIPT\_PROTOCOL](variables/RE_JAVASCRIPT_PROTOCOL.md) - [RE\_JS\_EXT](variables/RE_JS_EXT.md) +- [RE\_JS\_IDENTIFIER](variables/RE_JS_IDENTIFIER.md) +- [RE\_JSX\_ANNOTATION](variables/RE_JSX_ANNOTATION.md) - [RE\_KEBAB\_CASE](variables/RE_KEBAB_CASE.md) - [RE\_PASCAL\_CASE](variables/RE_PASCAL_CASE.md) - [RE\_REGEXP\_STR](variables/RE_REGEXP_STR.md) diff --git a/packages/utilities/kit/docs/functions/toRegExp.md b/packages/utilities/kit/docs/functions/toRegExp.md index 95fe8083d5..b1afa8c9b2 100644 --- a/packages/utilities/kit/docs/functions/toRegExp.md +++ b/packages/utilities/kit/docs/functions/toRegExp.md @@ -37,3 +37,7 @@ Returns the `RegExp`. #### Returns `boolean` + +## See + +https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts diff --git a/packages/utilities/kit/docs/variables/RE_JSX_ANNOTATION.md b/packages/utilities/kit/docs/variables/RE_JSX_ANNOTATION.md new file mode 100644 index 0000000000..e41758faa6 --- /dev/null +++ b/packages/utilities/kit/docs/variables/RE_JSX_ANNOTATION.md @@ -0,0 +1,11 @@ +[**@eslint-react/kit**](../README.md) + +*** + +[@eslint-react/kit](../README.md) / RE\_JSX\_ANNOTATION + +# Variable: RE\_JSX\_ANNOTATION + +> `const` **RE\_JSX\_ANNOTATION**: [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp) + +Regular expression for matching a JSX pragma comment. diff --git a/packages/utilities/kit/docs/variables/RE_JS_IDENTIFIER.md b/packages/utilities/kit/docs/variables/RE_JS_IDENTIFIER.md new file mode 100644 index 0000000000..d1a918a72d --- /dev/null +++ b/packages/utilities/kit/docs/variables/RE_JS_IDENTIFIER.md @@ -0,0 +1,11 @@ +[**@eslint-react/kit**](../README.md) + +*** + +[@eslint-react/kit](../README.md) / RE\_JS\_IDENTIFIER + +# Variable: RE\_JS\_IDENTIFIER + +> `const` **RE\_JS\_IDENTIFIER**: [`RegExp`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp) + +Regular expression for matching a valid JavaScript identifier. diff --git a/packages/utilities/kit/src/RegExp.ts b/packages/utilities/kit/src/RegExp.ts index 145d09661e..bb6cb23ba2 100644 --- a/packages/utilities/kit/src/RegExp.ts +++ b/packages/utilities/kit/src/RegExp.ts @@ -38,7 +38,15 @@ export const RE_CONSTANT_CASE = /^[A-Z][\d_A-Z]*$/u; // eslint-disable-next-line no-control-regex export const RE_JAVASCRIPT_PROTOCOL = /^[\u0000-\u001F ]*j[\t\n\r]*a[\t\n\r]*v[\t\n\r]*a[\t\n\r]*s[\t\n\r]*c[\t\n\r]*r[\t\n\r]*i[\t\n\r]*p[\t\n\r]*t[\t\n\r]*:/iu; -// Ported from https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts +/** + * Regular expression for matching a JSX pragma comment. + */ +export const RE_JSX_ANNOTATION = /@jsx\s+(\S+)/; + +/** + * Regular expression for matching a valid JavaScript identifier. + */ +export const RE_JS_IDENTIFIER = /^[_$a-z][\w$]*$/i; /** * Regular expression for matching a RegExp string. @@ -49,7 +57,7 @@ export const RE_REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u; * Convert a string to the `RegExp`. * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`. * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`. - * + * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts * @param string The string to convert. * @returns Returns the `RegExp`. */