Skip to content

Commit 49bbab0

Browse files
authored
feat(react-x): enhance jsx-uses-react rule to support inline jsx annotation (#1029)
1 parent 79eb18f commit 49bbab0

File tree

9 files changed

+265
-123
lines changed

9 files changed

+265
-123
lines changed
Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,81 @@
1+
import { RuleTester } from "@typescript-eslint/rule-tester";
12
import tsx from "dedent";
23

3-
import { allValid, ruleTester } from "../../../../../test";
4-
import rule from "./jsx-uses-react";
4+
import { JsxEmit } from "typescript";
5+
import { defaultLanguageOptionsWithTypes, getProjectForJsxRuntime } from "../../../../../test";
6+
import rule, { debug, RULE_NAME } from "./jsx-uses-react";
57

6-
ruleTester.run("no-unused-vars", rule, {
7-
// TODO: Add invalid test cases
8-
invalid: [],
9-
valid: [
10-
...allValid,
11-
{
8+
const ruleTester = new RuleTester({
9+
languageOptions: {
10+
...defaultLanguageOptionsWithTypes,
11+
parserOptions: {
12+
...defaultLanguageOptionsWithTypes.parserOptions,
13+
project: getProjectForJsxRuntime(JsxEmit.React),
14+
projectService: false,
15+
},
16+
},
17+
});
18+
19+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions, @typescript-eslint/no-unnecessary-condition
20+
debug
21+
? ruleTester.run(RULE_NAME, rule, {
22+
invalid: [
23+
{
24+
code: tsx`
25+
import React from "react";
26+
27+
const Hello = <div>Hello</div>;
28+
29+
console.log(Hello);
30+
`,
31+
errors: [
32+
{
33+
messageId: "jsxUsesReact",
34+
data: { name: "React" },
35+
},
36+
{
37+
messageId: "jsxUsesReact",
38+
data: { name: "React.createElement" },
39+
},
40+
],
41+
},
42+
{
43+
code: tsx`
44+
/** @jsx Foo */
45+
import Foo from "foo";
46+
47+
const Hello = <div>Hello</div>;
48+
49+
console.log(Hello);
50+
`,
51+
errors: [
52+
{
53+
messageId: "jsxUsesReact",
54+
data: { name: "Foo" },
55+
},
56+
],
57+
},
58+
],
59+
valid: [],
60+
})
61+
: ruleTester.run(RULE_NAME, rule, {
62+
invalid: [],
63+
valid: [{
1264
code: tsx`
1365
import React from "react";
1466
1567
const Hello = <div>Hello</div>;
68+
69+
console.log(Hello);
1670
`,
17-
},
18-
{
71+
}, {
1972
code: tsx`
2073
/** @jsx Foo */
2174
import Foo from "foo";
2275
2376
const Hello = <div>Hello</div>;
77+
78+
console.log(Hello);
2479
`,
25-
},
26-
],
27-
});
80+
}],
81+
});
Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import type { TSESTree } from "@typescript-eslint/types";
12
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
2-
import type { CamelCase } from "string-ts";
3-
import { JsxRuntime, type RuleContext, type RuleFeature } from "@eslint-react/kit";
43

4+
import type { CamelCase } from "string-ts";
5+
import {
6+
JsxRuntime,
7+
RE_JSX_ANNOTATION,
8+
RE_JSX_FRAG_ANNOTATION,
9+
type RuleContext,
10+
type RuleFeature,
11+
} from "@eslint-react/kit";
512
import { JsxEmit } from "typescript";
613
import { createRule } from "../utils";
714

@@ -11,6 +18,8 @@ export const RULE_FEATURES = [] as const satisfies RuleFeature[];
1118

1219
export type MessageID = CamelCase<typeof RULE_NAME>;
1320

21+
export const debug = true;
22+
1423
export default createRule<[], MessageID>({
1524
meta: {
1625
type: "problem",
@@ -19,7 +28,7 @@ export default createRule<[], MessageID>({
1928
[Symbol.for("rule_features")]: RULE_FEATURES,
2029
},
2130
messages: {
22-
jsxUsesReact: "",
31+
jsxUsesReact: "Marked {{name}} as used.",
2332
},
2433
schema: [],
2534
},
@@ -30,19 +39,59 @@ export default createRule<[], MessageID>({
3039

3140
export function create(context: RuleContext<MessageID, []>): RuleListener {
3241
const { jsx, jsxFactory, jsxFragmentFactory, reactNamespace } = JsxRuntime.getJsxRuntimeOptionsFromContext(context);
33-
// If we are using the New JSX Transform, this rule should do nothing.
34-
if (jsx === JsxEmit.ReactJSX || jsx === JsxEmit.ReactJSXDev) return {};
35-
return {
36-
JSXFragment(node) {
37-
context.sourceCode.markVariableAsUsed(jsxFragmentFactory, node);
38-
},
39-
JSXOpeningElement(node) {
40-
context.sourceCode.markVariableAsUsed(reactNamespace, node);
41-
context.sourceCode.markVariableAsUsed(jsxFactory, node);
42-
},
43-
JSXOpeningFragment(node) {
42+
const jsxAnnotation = getJsxAnnotation(context);
43+
if (jsx === JsxEmit.ReactJSX || jsx === JsxEmit.ReactJSXDev || jsx === JsxEmit.Preserve) return {};
44+
45+
function handleJsxElement(node: TSESTree.Node) {
46+
if (jsxAnnotation == null) {
4447
context.sourceCode.markVariableAsUsed(reactNamespace, node);
4548
context.sourceCode.markVariableAsUsed(jsxFactory, node);
46-
},
49+
debugReport(context, node, reactNamespace);
50+
debugReport(context, node, jsxFactory);
51+
}
52+
if (jsxAnnotation?.jsx != null) {
53+
context.sourceCode.markVariableAsUsed(jsxAnnotation.jsx, node);
54+
debugReport(context, node, jsxAnnotation.jsx);
55+
}
56+
}
57+
58+
function handleJsxFragment(node: TSESTree.Node) {
59+
if (jsxAnnotation == null) {
60+
context.sourceCode.markVariableAsUsed(jsxFragmentFactory, node);
61+
debugReport(context, node, jsxFragmentFactory);
62+
}
63+
if (jsxAnnotation?.jsxFrag != null) {
64+
context.sourceCode.markVariableAsUsed(jsxAnnotation.jsxFrag, node);
65+
debugReport(context, node, jsxAnnotation.jsxFrag);
66+
}
67+
}
68+
69+
return {
70+
JSXFragment: handleJsxFragment,
71+
JSXOpeningElement: handleJsxElement,
72+
JSXOpeningFragment: handleJsxElement,
73+
};
74+
}
75+
76+
function getJsxAnnotation(context: RuleContext) {
77+
if (!context.sourceCode.text.includes("@jsx")) return;
78+
const allComments = context.sourceCode.getAllComments();
79+
const jsxComment = allComments.find((n) => RE_JSX_ANNOTATION.test(n.value));
80+
const jsxFragComment = allComments.find((n) => RE_JSX_FRAG_ANNOTATION.test(n.value));
81+
const jsx = jsxComment?.value.match(RE_JSX_ANNOTATION)?.[1];
82+
const jsxFrag = jsxFragComment?.value.match(RE_JSX_FRAG_ANNOTATION)?.[1];
83+
return {
84+
jsx,
85+
jsxFrag,
4786
};
4887
}
88+
89+
function debugReport(context: RuleContext, node: TSESTree.Node, name: string) {
90+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
91+
if (!debug) return;
92+
context.report({
93+
messageId: "jsxUsesReact",
94+
node,
95+
data: { name },
96+
});
97+
}
Lines changed: 5 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,10 @@
1-
import tsx from "dedent";
1+
import { ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./jsx-uses-vars";
23

3-
import { allValid, ruleTester } from "../../../../../test";
4-
import rule from "./jsx-uses-vars";
5-
6-
ruleTester.run("no-unused-vars", rule, {
7-
// TODO: Add invalid test cases
4+
// TODO: Add tests
5+
ruleTester.run(RULE_NAME, rule, {
86
invalid: [],
97
valid: [
10-
...allValid,
11-
{
12-
code: tsx`
13-
function foo() {
14-
var App;
15-
var bar = React.render(<App/>);
16-
return bar;
17-
};
18-
foo()
19-
`,
20-
},
21-
{
22-
code: tsx`
23-
var App;
24-
React.render(<App/>);
25-
`,
26-
},
27-
{
28-
code: tsx`
29-
var a = 1;
30-
React.render(<img src={a} />);
31-
`,
32-
},
33-
{
34-
code: tsx`
35-
var App;
36-
function f() {
37-
return <App />;
38-
}
39-
f();
40-
`,
41-
},
42-
{
43-
code: tsx`
44-
var App;
45-
<App.Hello />
46-
`,
47-
},
48-
{
49-
code: tsx`
50-
class HelloMessage {};
51-
<HelloMessage />
52-
`,
53-
},
54-
{
55-
code: tsx`
56-
class HelloMessage {
57-
render() {
58-
var HelloMessage = <div>Hello</div>;
59-
return HelloMessage;
60-
}
61-
};
62-
<HelloMessage />
63-
`,
64-
},
65-
{
66-
code: tsx`
67-
function foo() {
68-
var App = { Foo: { Bar: {} } };
69-
var bar = React.render(<App.Foo.Bar/>);
70-
return bar;
71-
};
72-
foo()
73-
`,
74-
},
75-
{
76-
code: tsx`
77-
function foo() {
78-
var App = { Foo: { Bar: { Baz: {} } } };
79-
var bar = React.render(<App.Foo.Bar.Baz/>);
80-
return bar;
81-
};
82-
foo()
83-
`,
84-
},
85-
{
86-
code: tsx`
87-
var object;
88-
React.render(<object.Tag />);
89-
`,
90-
},
91-
{
92-
code: tsx`
93-
var object;
94-
React.render(<object.tag />);
95-
`,
96-
},
8+
"const a = <div />;",
979
],
9810
});

packages/utilities/kit/src/JsxRuntime/JsxRuntimeOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type JsxRuntimeOptions = Pick<
2222
*/
2323
export function getJsxRuntimeOptionsFromContext(context: RuleContext) {
2424
const options = context.sourceCode.parserServices?.program?.getCompilerOptions() ?? {};
25+
console.log(options);
2526
return {
2627
jsx: options.jsx ?? JsxEmit.ReactJSX,
2728
jsxFactory: options.jsxFactory ?? "React.createElement",

packages/utilities/kit/src/RegExp.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ export const RE_CONSTANT_CASE = /^[A-Z][\d_A-Z]*$/u;
3939
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;
4040

4141
/**
42-
* Regular expression for matching a JSX pragma comment.
42+
* Regular expression for matching a `@jsx` annotation comment.
4343
*/
44-
export const RE_JSX_ANNOTATION = /@jsx\s+(\S+)/;
44+
export const RE_JSX_ANNOTATION = /@jsx\s+(\S+)/u;
45+
46+
/**
47+
* Regular expression for matching a `@jsxFrag` annotation comment.
48+
*/
49+
export const RE_JSX_FRAG_ANNOTATION = /@jsxFrag\s+(\S+)/u;
4550

4651
/**
4752
* Regular expression for matching a valid JavaScript identifier.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"extends": [
3+
"@tsconfig/strictest/tsconfig.json"
4+
],
5+
"compilerOptions": {
6+
"target": "ES2021",
7+
"useDefineForClassFields": true,
8+
"lib": [
9+
"ES2021",
10+
"DOM",
11+
"DOM.Iterable"
12+
],
13+
"module": "ESNext",
14+
"skipLibCheck": true,
15+
"moduleDetection": "force",
16+
"moduleResolution": "bundler",
17+
"allowImportingTsExtensions": true,
18+
"resolveJsonModule": true,
19+
"isolatedModules": true,
20+
"noEmit": true,
21+
"jsx": "preserve",
22+
"strict": true,
23+
"erasableSyntaxOnly": true,
24+
"strictNullChecks": true,
25+
"noUnusedLocals": true,
26+
"noUnusedParameters": true,
27+
"noPropertyAccessFromIndexSignature": false,
28+
"noFallthroughCasesInSwitch": true,
29+
"incremental": false
30+
},
31+
"include": [
32+
"*.ts",
33+
"*.tsx"
34+
]
35+
}

0 commit comments

Comments
 (0)