Skip to content

Commit d151f30

Browse files
committed
refactor(eslint-plugin-react-compiler): add back files moved to react hooks
1 parent a6858bb commit d151f30

File tree

5 files changed

+786
-0
lines changed

5 files changed

+786
-0
lines changed
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {ErrorSeverity} from 'babel-plugin-react-compiler/src';
9+
import {RuleTester as ESLintTester} from 'eslint';
10+
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
11+
12+
/**
13+
* A string template tag that removes padding from the left side of multi-line strings
14+
* @param {Array} strings array of code strings (only one expected)
15+
*/
16+
function normalizeIndent(strings: TemplateStringsArray): string {
17+
const codeLines = strings[0].split('\n');
18+
const leftPadding = codeLines[1].match(/\s+/)![0];
19+
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
20+
}
21+
22+
type CompilerTestCases = {
23+
valid: ESLintTester.ValidTestCase[];
24+
invalid: ESLintTester.InvalidTestCase[];
25+
};
26+
27+
const tests: CompilerTestCases = {
28+
valid: [
29+
{
30+
name: 'Basic example',
31+
code: normalizeIndent`
32+
function foo(x, y) {
33+
if (x) {
34+
return foo(false, y);
35+
}
36+
return [y * 10];
37+
}
38+
`,
39+
},
40+
{
41+
name: 'Violation with Flow suppression',
42+
code: `
43+
// Valid since error already suppressed with flow.
44+
function useHookWithHook() {
45+
if (cond) {
46+
// $FlowFixMe[react-rule-hook]
47+
useConditionalHook();
48+
}
49+
}
50+
`,
51+
},
52+
{
53+
name: 'Basic example with component syntax',
54+
code: normalizeIndent`
55+
export default component HelloWorld(
56+
text: string = 'Hello!',
57+
onClick: () => void,
58+
) {
59+
return <div onClick={onClick}>{text}</div>;
60+
}
61+
`,
62+
},
63+
{
64+
name: 'Unsupported syntax',
65+
code: normalizeIndent`
66+
function foo(x) {
67+
var y = 1;
68+
return y * x;
69+
}
70+
`,
71+
},
72+
{
73+
// OK because invariants are only meant for the compiler team's consumption
74+
name: '[Invariant] Defined after use',
75+
code: normalizeIndent`
76+
function Component(props) {
77+
let y = function () {
78+
m(x);
79+
};
80+
81+
let x = { a };
82+
m(x);
83+
return y;
84+
}
85+
`,
86+
},
87+
{
88+
name: "Classes don't throw",
89+
code: normalizeIndent`
90+
class Foo {
91+
#bar() {}
92+
}
93+
`,
94+
},
95+
{
96+
// Don't report the issue if Flow already has
97+
name: '[InvalidInput] Ref access during render',
98+
code: normalizeIndent`
99+
function Component(props) {
100+
const ref = useRef(null);
101+
// $FlowFixMe[react-rule-unsafe-ref]
102+
const value = ref.current;
103+
return value;
104+
}
105+
`,
106+
},
107+
],
108+
invalid: [
109+
{
110+
name: '[InvalidInput] Ref access during render',
111+
code: normalizeIndent`
112+
function Component(props) {
113+
const ref = useRef(null);
114+
const value = ref.current;
115+
return value;
116+
}
117+
`,
118+
errors: [
119+
{
120+
message:
121+
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
122+
},
123+
],
124+
},
125+
{
126+
name: 'Reportable levels can be configured',
127+
options: [{reportableLevels: new Set([ErrorSeverity.Todo])}],
128+
code: normalizeIndent`
129+
function Foo(x) {
130+
var y = 1;
131+
return <div>{y * x}</div>;
132+
}`,
133+
errors: [
134+
{
135+
message:
136+
'(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration',
137+
},
138+
],
139+
},
140+
{
141+
name: '[InvalidReact] ESlint suppression',
142+
// Indentation is intentionally weird so it doesn't add extra whitespace
143+
code: normalizeIndent`
144+
function Component(props) {
145+
// eslint-disable-next-line react-hooks/rules-of-hooks
146+
return <div>{props.foo}</div>;
147+
}`,
148+
errors: [
149+
{
150+
message:
151+
'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior',
152+
suggestions: [
153+
{
154+
output: normalizeIndent`
155+
function Component(props) {
156+
157+
return <div>{props.foo}</div>;
158+
}`,
159+
},
160+
],
161+
},
162+
{
163+
message:
164+
"Definition for rule 'react-hooks/rules-of-hooks' was not found.",
165+
},
166+
],
167+
},
168+
{
169+
name: 'Multiple diagnostics are surfaced',
170+
options: [
171+
{
172+
reportableLevels: new Set([
173+
ErrorSeverity.Todo,
174+
ErrorSeverity.InvalidReact,
175+
]),
176+
},
177+
],
178+
code: normalizeIndent`
179+
function Foo(x) {
180+
var y = 1;
181+
return <div>{y * x}</div>;
182+
}
183+
function Bar(props) {
184+
props.a.b = 2;
185+
return <div>{props.c}</div>
186+
}`,
187+
errors: [
188+
{
189+
message:
190+
'(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration',
191+
},
192+
{
193+
message:
194+
'Mutating component props or hook arguments is not allowed. Consider using a local variable instead',
195+
},
196+
],
197+
},
198+
{
199+
name: 'Test experimental/unstable report all bailouts mode',
200+
options: [
201+
{
202+
reportableLevels: new Set([ErrorSeverity.InvalidReact]),
203+
__unstable_donotuse_reportAllBailouts: true,
204+
},
205+
],
206+
code: normalizeIndent`
207+
function Foo(x) {
208+
var y = 1;
209+
return <div>{y * x}</div>;
210+
}`,
211+
errors: [
212+
{
213+
message:
214+
'[ReactCompilerBailout] (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (@:3:2)',
215+
},
216+
],
217+
},
218+
{
219+
name: "'use no forget' does not disable eslint rule",
220+
code: normalizeIndent`
221+
let count = 0;
222+
function Component() {
223+
'use no forget';
224+
count = count + 1;
225+
return <div>Hello world {count}</div>
226+
}
227+
`,
228+
errors: [
229+
{
230+
message:
231+
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
232+
},
233+
],
234+
},
235+
{
236+
name: "Unused 'use no forget' directive is reported when no errors are present on components",
237+
code: normalizeIndent`
238+
function Component() {
239+
'use no forget';
240+
return <div>Hello world</div>
241+
}
242+
`,
243+
errors: [
244+
{
245+
message: "Unused 'use no forget' directive",
246+
suggestions: [
247+
{
248+
output:
249+
// yuck
250+
'\nfunction Component() {\n \n return <div>Hello world</div>\n}\n',
251+
},
252+
],
253+
},
254+
],
255+
},
256+
{
257+
name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks",
258+
code: normalizeIndent`
259+
function notacomponent() {
260+
'use no forget';
261+
return 1 + 1;
262+
}
263+
`,
264+
errors: [
265+
{
266+
message: "Unused 'use no forget' directive",
267+
suggestions: [
268+
{
269+
output:
270+
// yuck
271+
'\nfunction notacomponent() {\n \n return 1 + 1;\n}\n',
272+
},
273+
],
274+
},
275+
],
276+
},
277+
],
278+
};
279+
280+
const eslintTester = new ESLintTester({
281+
parser: require.resolve('hermes-eslint'),
282+
parserOptions: {
283+
ecmaVersion: 2015,
284+
sourceType: 'module',
285+
enableExperimentalComponentSyntax: true,
286+
},
287+
});
288+
eslintTester.run('react-compiler', ReactCompilerRule, tests);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {RuleTester} from 'eslint';
9+
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
10+
11+
/**
12+
* A string template tag that removes padding from the left side of multi-line strings
13+
* @param {Array} strings array of code strings (only one expected)
14+
*/
15+
function normalizeIndent(strings: TemplateStringsArray): string {
16+
const codeLines = strings[0].split('\n');
17+
const leftPadding = codeLines[1].match(/\s+/)[0];
18+
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
19+
}
20+
21+
type CompilerTestCases = {
22+
valid: RuleTester.ValidTestCase[];
23+
invalid: RuleTester.InvalidTestCase[];
24+
};
25+
26+
const tests: CompilerTestCases = {
27+
valid: [
28+
{
29+
name: 'Basic example',
30+
filename: 'test.tsx',
31+
code: normalizeIndent`
32+
function Button(props) {
33+
return null;
34+
}
35+
`,
36+
},
37+
{
38+
name: 'Repro for hooks as normal values',
39+
filename: 'test.tsx',
40+
code: normalizeIndent`
41+
function Button(props) {
42+
const scrollview = React.useRef<ScrollView>(null);
43+
return <Button thing={scrollview} />;
44+
}
45+
`,
46+
},
47+
],
48+
invalid: [
49+
{
50+
name: 'Mutating useState value',
51+
filename: 'test.tsx',
52+
code: `
53+
import { useState } from 'react';
54+
function Component(props) {
55+
// typescript syntax that hermes-parser doesn't understand yet
56+
const x: \`foo\${1}\` = 'foo1';
57+
const [state, setState] = useState({a: 0});
58+
state.a = 1;
59+
return <div>{props.foo}</div>;
60+
}
61+
`,
62+
errors: [
63+
{
64+
message:
65+
"Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead",
66+
line: 7,
67+
},
68+
],
69+
},
70+
],
71+
};
72+
73+
const eslintTester = new RuleTester({
74+
parser: require.resolve('@typescript-eslint/parser'),
75+
});
76+
eslintTester.run('react-compiler', ReactCompilerRule, tests);

0 commit comments

Comments
 (0)