diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 82f7537aca..48ea474435 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -53,6 +53,7 @@ "prefer-shorthand-boolean", "prefer-shorthand-fragment", "jsx-no-duplicate-props", + "jsx-no-undef", "jsx-uses-vars", "---DOM Rules---", "dom-no-dangerously-set-innerhtml", diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index e5a92791a3..756b8da7eb 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -76,6 +76,7 @@ Linter rules can have false positives, false negatives, and some rules are depen | [`prefer-shorthand-boolean`](./prefer-shorthand-boolean) | 0️⃣ | `🔧` | Enforces shorthand syntax for boolean attributes | | | [`prefer-shorthand-fragment`](./prefer-shorthand-fragment) | 0️⃣ | `🔧` | Enforces shorthand syntax for fragments | | | [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ | | Disallow duplicate props in JSX elements | | +| [`jsx-no-undef`](./jsx-no-undef) | 2️⃣ | | Disallow undefined variables in JSX elements | | | [`jsx-uses-vars`](./jsx-uses-vars) | 1️⃣ | | Marks variables used in JSX as used | | ## DOM Rules 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 346fce04c6..8bf3053930 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 @@ -7,6 +7,7 @@ export const name = "react-x/recommended-typescript"; export const rules = { ...recommended.rules, "react-x/jsx-no-duplicate-props": "off", + "react-x/jsx-no-undef": "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 e9555ce863..ebc63b5f2e 100644 --- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts +++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts @@ -4,6 +4,9 @@ import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared"; export const name = "react-x/recommended"; export const rules = { + "react-x/jsx-no-duplicate-props": "warn", + "react-x/jsx-no-undef": "error", + "react-x/jsx-uses-vars": "warn", "react-x/no-access-state-in-setstate": "error", "react-x/no-array-index-key": "warn", "react-x/no-children-count": "warn", @@ -20,7 +23,6 @@ export const rules = { "react-x/no-create-ref": "error", "react-x/no-default-props": "error", "react-x/no-direct-mutation-state": "error", - "react-x/jsx-no-duplicate-props": "warn", "react-x/no-duplicate-key": "warn", "react-x/no-forward-ref": "warn", "react-x/no-implicit-key": "warn", @@ -41,7 +43,6 @@ export const rules = { "react-x/no-unused-state": "warn", "react-x/no-use-context": "warn", "react-x/no-useless-forward-ref": "warn", - "react-x/jsx-uses-vars": "warn", } as const satisfies RulePreset; export const settings = { diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index 29eb2b74c0..4e485b401e 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -2,6 +2,7 @@ import { name, version } from "../package.json"; 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 jsxUsesVars from "./rules/jsx-uses-vars"; import noAccessStateInSetstate from "./rules/no-access-state-in-setstate"; import noArrayIndexKey from "./rules/no-array-index-key"; @@ -111,6 +112,7 @@ export const plugin = { // Part: JSX only rules "jsx-no-duplicate-props": jsxNoDuplicateProps, + "jsx-no-undef": jsxNoUndef, "jsx-uses-vars": jsxUsesVars, // Part: deprecated rules diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.md b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.md new file mode 100644 index 0000000000..11a5264bc5 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.md @@ -0,0 +1,69 @@ +--- +title: jsx-no-undef +--- + +**Full Name in `eslint-plugin-react-x`** + +```plain copy +react-x/jsx-no-undef +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```plain copy +@eslint-react/jsx-no-undef +``` + +**Presets** + +- `core` +- `recommended` + +## Description + +This rule is used to prevent the use of undefined variables in JSX. It checks for any undefined variables in the JSX code and reports them as errors. + +## Examples + +### Failing + +```jsx +const MyComponent = () => { + return ( +
+ +
+ ); +}; +``` + +### Passing + +```jsx +import Foo from "./Foo"; + +const MyComponent = () => { + return ( +
+ +
+ ); +}; +``` + +```jsx +const Foo = () =>
Foo
; + +const MyComponent = () => { + return ( +
+ +
+ ); +}; +``` + +## Implementation + +- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts) +- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts) diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts new file mode 100644 index 0000000000..c2d99aea7d --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts @@ -0,0 +1,76 @@ +import tsx from "dedent"; + +import { ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./jsx-no-undef"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: tsx` + const element = ; + `, + errors: [ + { + messageId: "jsxNoUndef", + data: { name: "Foo" }, + }, + ], + }, + { + code: tsx` + const element = ; + const element = ; + `, + errors: [ + { + messageId: "jsxNoUndef", + data: { name: "Foo" }, + }, + { + messageId: "jsxNoUndef", + data: { name: "Bar" }, + }, + ], + }, + { + code: tsx` + function Foo() { + return ; + } + `, + errors: [ + { + messageId: "jsxNoUndef", + data: { name: "Bar" }, + }, + ], + }, + ], + valid: [ + { + code: tsx` + function Foo() { + return
; + } + function Bar() { + return ; + } + `, + }, + { + code: tsx` + import { Foo } from "./Foo"; + import { Bar } from "./Bar"; + + function App() { + return ( +
+ + +
+ ); + } + `, + }, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts new file mode 100644 index 0000000000..e8d914a424 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts @@ -0,0 +1,56 @@ +import type { RuleContext, RuleFeature } from "@eslint-react/kit"; +import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { CamelCase } from "string-ts"; +import * as VAR from "@eslint-react/var"; + +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import { createRule } from "../utils"; + +export const RULE_NAME = "jsx-no-undef"; + +export const RULE_FEATURES = [] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "Disallow undefined variables in JSX.", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + messages: { + jsxNoUndef: "JSX variable {{name}} is not defined.", + }, + schema: [], + }, + name: RULE_NAME, + create, + defaultOptions: [], +}); + +export function create(context: RuleContext): RuleListener { + return { + JSXIdentifier(node) { + if (node.name === "this") { + return; + } + // Skip JsxIntrinsicElements + if (/^[a-z]/u.test(node.name)) { + return; + } + // Skip JSXMemberExpression property + if (node.parent.type === T.JSXMemberExpression && node.parent.property === node) { + return; + } + const initialScope = context.sourceCode.getScope(node); + if (VAR.findVariable(node.name, initialScope) == null) { + context.report({ + messageId: "jsxNoUndef", + node, + data: { name: node.name }, + }); + } + }, + }; +} diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts index dc537151ed..3b356743f7 100644 --- a/packages/plugins/eslint-plugin/src/configs/all.ts +++ b/packages/plugins/eslint-plugin/src/configs/all.ts @@ -12,6 +12,9 @@ export const name = "@eslint-react/all"; export const rules = { "@eslint-react/avoid-shorthand-boolean": "off", "@eslint-react/avoid-shorthand-fragment": "off", + "@eslint-react/jsx-no-duplicate-props": "warn", + "@eslint-react/jsx-no-undef": "error", + "@eslint-react/jsx-uses-vars": "warn", "@eslint-react/no-access-state-in-setstate": "error", "@eslint-react/no-array-index-key": "warn", "@eslint-react/no-children-count": "warn", @@ -31,7 +34,6 @@ export const rules = { "@eslint-react/no-create-ref": "error", "@eslint-react/no-default-props": "error", "@eslint-react/no-direct-mutation-state": "error", - "@eslint-react/jsx-no-duplicate-props": "warn", "@eslint-react/no-duplicate-key": "warn", "@eslint-react/no-forward-ref": "warn", "@eslint-react/no-implicit-key": "warn", @@ -58,7 +60,6 @@ export const rules = { "@eslint-react/prefer-destructuring-assignment": "warn", "@eslint-react/prefer-shorthand-boolean": "warn", "@eslint-react/prefer-shorthand-fragment": "warn", - "@eslint-react/jsx-uses-vars": "warn", "@eslint-react/dom/no-dangerously-set-innerhtml": "warn", "@eslint-react/dom/no-dangerously-set-innerhtml-with-children": "error", diff --git a/packages/plugins/eslint-plugin/src/configs/core.ts b/packages/plugins/eslint-plugin/src/configs/core.ts index 4d8c674cbc..b361fed188 100644 --- a/packages/plugins/eslint-plugin/src/configs/core.ts +++ b/packages/plugins/eslint-plugin/src/configs/core.ts @@ -5,6 +5,9 @@ import reactx from "eslint-plugin-react-x"; export const name = "@eslint-react/core"; export const rules = { + "@eslint-react/jsx-no-duplicate-props": "warn", + "@eslint-react/jsx-no-undef": "error", + "@eslint-react/jsx-uses-vars": "warn", "@eslint-react/no-access-state-in-setstate": "error", "@eslint-react/no-array-index-key": "warn", "@eslint-react/no-children-count": "warn", @@ -21,7 +24,6 @@ export const rules = { "@eslint-react/no-create-ref": "error", "@eslint-react/no-default-props": "error", "@eslint-react/no-direct-mutation-state": "error", - "@eslint-react/jsx-no-duplicate-props": "warn", "@eslint-react/no-duplicate-key": "warn", "@eslint-react/no-forward-ref": "warn", "@eslint-react/no-implicit-key": "warn", @@ -43,7 +45,6 @@ export const rules = { "@eslint-react/no-use-context": "warn", "@eslint-react/no-useless-forward-ref": "warn", "@eslint-react/no-useless-fragment": "warn", - "@eslint-react/jsx-uses-vars": "warn", } as const satisfies RulePreset; export const plugins = {