Skip to content

Commit c21ebb8

Browse files
authored
feat: add react-debug/jsx rule (#1041)
1 parent bc9ac77 commit c21ebb8

File tree

25 files changed

+315
-92
lines changed

25 files changed

+315
-92
lines changed

apps/website/content/docs/configuration/configure-language-config.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "Configure Language Config"
33
---
44

5-
import { JSXRuntimeTypeTable } from "./configure-language-config";
5+
import { JSXConfigTypeTable } from "./configure-language-config";
66

77
ESLint React references your language config to determine how to perform static code analysis. This page details which parts of ESLint React utilize your language config.
88

@@ -12,7 +12,7 @@ ESLint React reads your `tsconfig.json` or `jsconfig.json` configuration file to
1212

1313
The following [compiler options](https://www.typescriptlang.org/tsconfig/#compilerOptions) are respected:
1414

15-
<JSXRuntimeTypeTable />
15+
<JSXConfigTypeTable />
1616

1717
**If nothing is specified, it defaults to [`automatic`](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)**.
1818

apps/website/content/docs/configuration/configure-language-config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TypeTable } from "fumadocs-ui/components/type-table";
44

55
import { Link } from "next-view-transitions";
66

7-
export function JSXRuntimeTypeTable() {
7+
export function JSXConfigTypeTable() {
88
return (
99
<TypeTable
1010
type={{

apps/website/content/docs/rules/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"debug-class-component",
9696
"debug-function-component",
9797
"debug-hook",
98-
"debug-is-from-react"
98+
"debug-is-from-react",
99+
"debug-jsx"
99100
]
100101
}

apps/website/content/docs/rules/overview.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ Linter rules can have false positives, false negatives, and some rules are depen
139139
| [`function-component`](./debug-function-component) | 0️⃣ | `🐞` | Reports all function components |
140140
| [`hook`](./debug-hook) | 0️⃣ | `🐞` | Reports all react hooks |
141141
| [`is-from-react`](./debug-is-from-react) | 0️⃣ | `🐞` | Reports all identifiers that are initialized from React |
142+
| [`jsx`](./debug-jsx) | 0️⃣ | `🐞` | Reports all JSX elements |
142143

143144
## References
144145

packages/plugins/eslint-plugin-react-debug/src/configs/all.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export const rules = {
77
"react-debug/class-component": "warn",
88
"react-debug/function-component": "warn",
99
"react-debug/hook": "warn",
10-
"react-debug/is-from-react": "off",
10+
"react-debug/is-from-react": "warn",
11+
"react-debug/jsx": "warn",
1112
} as const satisfies RulePreset;
1213

1314
export const settings = {

packages/plugins/eslint-plugin-react-debug/src/plugin.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ import classComponent from "./rules/class-component";
33
import functionComponent from "./rules/function-component";
44
import hook from "./rules/hook";
55
import isFromReact from "./rules/is-from-react";
6+
import jsx from "./rules/jsx";
67

78
export const plugin = {
89
meta: {
910
name,
1011
version,
1112
},
1213
rules: {
13-
"class-component": classComponent,
14-
"function-component": functionComponent,
15-
hook: hook,
16-
"is-from-react": isFromReact,
14+
["class-component"]: classComponent,
15+
["function-component"]: functionComponent,
16+
["hook"]: hook,
17+
["is-from-react"]: isFromReact,
18+
["jsx"]: jsx,
1719

1820
// Part: deprecated rules
1921
/** @deprecated Use `hook` instead */
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
title: jsx
3+
---
4+
5+
**Full Name in `eslint-plugin-react-debug`**
6+
7+
```plain copy
8+
react-debug/jsx
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```plain copy
14+
@eslint-react/debug/jsx
15+
```
16+
17+
**Features**
18+
19+
`🐞`
20+
21+
**Presets**
22+
23+
- `debug`
24+
25+
## Description
26+
27+
Reports all JSX elements. Useful for debugging. This rule should only be used for debugging purposes. Otherwise, leave it off.
28+
29+
## Examples
30+
31+
```json title="tsconfig.json"
32+
{
33+
"compilerOptions": {
34+
"jsx": "react-jsx",
35+
"jsxFactory": "React.createElement",
36+
"jsxFragmentFactory": "React.Fragment",
37+
"jsxImportSource": "react"
38+
}
39+
}
40+
```
41+
42+
```tsx
43+
import React from "react";
44+
45+
const element = <div>Hello World</div>;
46+
// ^^^^^^^^^^^^^^^^^^^^^^
47+
// - [jsx] jsx: 'react-jsx', jsxFactory: 'React.createElement', jsxFragmentFactory: 'React.Fragment', jsxRuntime: 'automatic', jsxImportSource: 'react'
48+
```
49+
50+
```tsx
51+
/** @jsx Preact.h */
52+
/** @jsxFrag Preact.Fragment */
53+
/** @jsxImportSource preact */
54+
/** @jsxRuntime classic */
55+
56+
import Preact from "preact";
57+
58+
const element = <div>Hello World</div>;
59+
// ^^^^^^^^^^^^^^^^^^^^^^
60+
// - [jsx] jsx: 'react', jsxFactory: 'Preact.h', jsxFragmentFactory: 'Preact.Fragment', jsxImportSource: 'preact', jsxRuntime: 'classic', jsxImportSource: 'preact'
61+
```
62+
63+
## Implementation
64+
65+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-debug/src/rules/jsx-runtime.ts)
66+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-debug/src/rules/jsx-runtime.spec.ts)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { RuleTester } from "@typescript-eslint/rule-tester";
2+
import tsx from "dedent";
3+
4+
import { JsxEmit } from "typescript";
5+
import { defaultLanguageOptionsWithTypes, getProjectForJsxEmit } from "../../../../../test";
6+
7+
import rule, { RULE_NAME } from "./jsx";
8+
9+
const ruleTester = new RuleTester({
10+
languageOptions: {
11+
...defaultLanguageOptionsWithTypes,
12+
parserOptions: {
13+
...defaultLanguageOptionsWithTypes.parserOptions,
14+
project: getProjectForJsxEmit(JsxEmit.ReactJSX),
15+
projectService: false,
16+
},
17+
},
18+
});
19+
20+
ruleTester.run(RULE_NAME, rule, {
21+
invalid: [
22+
{
23+
code: tsx`
24+
import React from "react";
25+
const Foo = () => <div />;
26+
`,
27+
errors: [
28+
{
29+
messageId: "jsx",
30+
data: {
31+
jsx: JsxEmit.ReactJSX,
32+
jsxFactory: "React.createElement",
33+
jsxFragmentFactory: "React.Fragment",
34+
jsxImportSource: "react",
35+
jsxRuntime: "automatic",
36+
},
37+
},
38+
],
39+
},
40+
{
41+
code: tsx`
42+
/** @jsx Preact.h */
43+
/** @jsxFrag Preact.Fragment */
44+
/** @jsxImportSource preact */
45+
/** @jsxRuntime classic */
46+
import React from "react";
47+
const Foo = () => <div />;
48+
`,
49+
errors: [
50+
{
51+
messageId: "jsx",
52+
data: {
53+
jsx: JsxEmit.React,
54+
jsxFactory: "Preact.h",
55+
jsxFragmentFactory: "Preact.Fragment",
56+
jsxImportSource: "preact",
57+
jsxRuntime: "classic",
58+
},
59+
},
60+
],
61+
},
62+
{
63+
code: tsx`
64+
/** @jsxRuntime classic */
65+
import React from "react";
66+
const Foo = () => <div />;
67+
`,
68+
errors: [
69+
{
70+
messageId: "jsx",
71+
data: {
72+
jsx: JsxEmit.React,
73+
jsxFactory: "React.createElement",
74+
jsxFragmentFactory: "React.Fragment",
75+
jsxImportSource: "react",
76+
jsxRuntime: "classic",
77+
},
78+
},
79+
],
80+
},
81+
],
82+
valid: [],
83+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
2+
import type { CamelCase } from "string-ts";
3+
import { JsxConfig, type RuleContext, type RuleFeature } from "@eslint-react/kit";
4+
import { match, P } from "ts-pattern";
5+
6+
import { JsxEmit } from "typescript";
7+
import { createRule } from "../utils";
8+
9+
export const RULE_NAME = "jsx";
10+
11+
export const RULE_FEATURES = [
12+
"DBG",
13+
] as const satisfies RuleFeature[];
14+
15+
export type MessageID = CamelCase<typeof RULE_NAME>;
16+
17+
export default createRule<[], MessageID>({
18+
meta: {
19+
type: "problem",
20+
docs: {
21+
description: "Reports all React Hooks.",
22+
[Symbol.for("rule_features")]: RULE_FEATURES,
23+
},
24+
messages: {
25+
jsx:
26+
"[jsx] jsx: '{{jsx}}', jsxFactory: '{{jsxFactory}}', jsxFragmentFactory: '{{jsxFragmentFactory}}', jsxRuntime: '{{jsxRuntime}}' jsxImportSource: '{{jsxImportSource}}'",
27+
},
28+
schema: [],
29+
},
30+
name: RULE_NAME,
31+
create,
32+
defaultOptions: [],
33+
});
34+
35+
export function create(context: RuleContext<MessageID, []>): RuleListener {
36+
const jsxConfigFromContext = JsxConfig.getFromContext(context);
37+
const jsxConfigFromAnnotation = JsxConfig.getFromAnnotation(context);
38+
const jsxConfig = {
39+
...jsxConfigFromContext,
40+
...jsxConfigFromAnnotation,
41+
};
42+
43+
const baseDescriptor = {
44+
messageId: "jsx",
45+
data: {
46+
jsx: jsxConfig.jsx,
47+
jsxFactory: jsxConfig.jsxFactory,
48+
jsxFragmentFactory: jsxConfig.jsxFragmentFactory,
49+
jsxImportSource: jsxConfig.jsxImportSource,
50+
jsxRuntime: match<JsxEmit>(jsxConfig.jsx)
51+
.with(P.union(JsxEmit.None, JsxEmit.ReactJSX, JsxEmit.ReactJSXDev), () => "automatic")
52+
.otherwise(() => "classic"),
53+
},
54+
} as const;
55+
return {
56+
JSXElement(node) {
57+
context.report({
58+
...baseDescriptor,
59+
node,
60+
});
61+
},
62+
JSXFragment(node) {
63+
context.report({
64+
...baseDescriptor,
65+
node,
66+
});
67+
},
68+
};
69+
}

packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { RuleTester } from "@typescript-eslint/rule-tester";
22
import tsx from "dedent";
33

44
import { JsxEmit } from "typescript";
5-
import { defaultLanguageOptionsWithTypes, getProjectForJsxRuntime } from "../../../../../test";
5+
import { defaultLanguageOptionsWithTypes, getProjectForJsxEmit } from "../../../../../test";
66
import rule, { RULE_NAME } from "./jsx-uses-react";
77

88
const ruleTester = new RuleTester({
99
languageOptions: {
1010
...defaultLanguageOptionsWithTypes,
1111
parserOptions: {
1212
...defaultLanguageOptionsWithTypes.parserOptions,
13-
project: getProjectForJsxRuntime(JsxEmit.React),
13+
project: getProjectForJsxEmit(JsxEmit.React),
1414
projectService: false,
1515
},
1616
},

0 commit comments

Comments
 (0)