Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (facebook#35589)
Add a fast heuristic to detect whether a file may contain React components or hooks before running the full compiler. This avoids the overhead of Babel AST parsing and compilation for utility files, config files, and other non-React code. The heuristic uses ESLint's already-parsed AST to check for functions with React-like names at module scope: - Capitalized functions: MyComponent, Button, App - Hook pattern functions: useEffect, useState, useMyCustomHook Files without matching function names are skipped and return an empty result, which is cached to avoid re-checking for subsequent rules. Also adds test coverage for the heuristic edge cases.
1 parent cdbd55f commit 03ee29d

File tree

4 files changed

+390
-0
lines changed

4 files changed

+390
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 {allRules} from '../src/shared/ReactCompiler';
10+
11+
const ESLintTesterV8 = require('eslint-v8').RuleTester;
12+
13+
/**
14+
* A string template tag that removes padding from the left side of multi-line strings
15+
* @param {Array} strings array of code strings (only one expected)
16+
*/
17+
function normalizeIndent(strings: TemplateStringsArray): string {
18+
const codeLines = strings[0]?.split('\n') ?? [];
19+
const leftPadding = codeLines[1]?.match(/\s+/)![0] ?? '';
20+
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
type CompilerTestCases = {
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
const tests: CompilerTestCases = {
29+
valid: [
30+
// ===========================================
31+
// Tests for mayContainReactCode heuristic with Flow syntax
32+
// Files that should be SKIPPED (no React-like function names)
33+
// These contain code that WOULD trigger errors if compiled,
34+
// but since the heuristic skips them, no errors are reported.
35+
// ===========================================
36+
{
37+
name: '[Heuristic/Flow] Skips files with only lowercase utility functions',
38+
filename: 'utils.js',
39+
code: normalizeIndent`
40+
function helper(obj) {
41+
obj.key = 'value';
42+
return obj;
43+
}
44+
`,
45+
},
46+
{
47+
name: '[Heuristic/Flow] Skips lowercase arrow functions even with mutations',
48+
filename: 'helpers.js',
49+
code: normalizeIndent`
50+
const processData = (input) => {
51+
input.modified = true;
52+
return input;
53+
};
54+
`,
55+
},
56+
],
57+
invalid: [
58+
// ===========================================
59+
// Tests for mayContainReactCode heuristic with Flow component/hook syntax
60+
// These use Flow's component/hook declarations which should be detected
61+
// ===========================================
62+
{
63+
name: '[Heuristic/Flow] Compiles Flow component declaration - detects prop mutation',
64+
filename: 'component.js',
65+
code: normalizeIndent`
66+
component MyComponent(a: {key: string}) {
67+
a.key = 'value';
68+
return <div />;
69+
}
70+
`,
71+
errors: [
72+
{
73+
message: /Modifying component props/,
74+
},
75+
],
76+
},
77+
{
78+
name: '[Heuristic/Flow] Compiles exported Flow component declaration - detects prop mutation',
79+
filename: 'component.js',
80+
code: normalizeIndent`
81+
export component MyComponent(a: {key: string}) {
82+
a.key = 'value';
83+
return <div />;
84+
}
85+
`,
86+
errors: [
87+
{
88+
message: /Modifying component props/,
89+
},
90+
],
91+
},
92+
{
93+
name: '[Heuristic/Flow] Compiles default exported Flow component declaration - detects prop mutation',
94+
filename: 'component.js',
95+
code: normalizeIndent`
96+
export default component MyComponent(a: {key: string}) {
97+
a.key = 'value';
98+
return <div />;
99+
}
100+
`,
101+
errors: [
102+
{
103+
message: /Modifying component props/,
104+
},
105+
],
106+
},
107+
{
108+
name: '[Heuristic/Flow] Compiles Flow hook declaration - detects argument mutation',
109+
filename: 'hooks.js',
110+
code: normalizeIndent`
111+
hook useMyHook(a: {key: string}) {
112+
a.key = 'value';
113+
return a;
114+
}
115+
`,
116+
errors: [
117+
{
118+
message: /Modifying component props or hook arguments/,
119+
},
120+
],
121+
},
122+
{
123+
name: '[Heuristic/Flow] Compiles exported Flow hook declaration - detects argument mutation',
124+
filename: 'hooks.js',
125+
code: normalizeIndent`
126+
export hook useMyHook(a: {key: string}) {
127+
a.key = 'value';
128+
return a;
129+
}
130+
`,
131+
errors: [
132+
{
133+
message: /Modifying component props or hook arguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
const eslintTester = new ESLintTesterV8({
141+
parser: require.resolve('hermes-eslint'),
142+
parserOptions: {
143+
sourceType: 'module',
144+
enableExperimentalComponentSyntax: true,
145+
},
146+
});
147+
eslintTester.run('react-compiler', allRules['immutability'].rule, tests);

packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,35 @@ const tests: CompilerTestCases = {
4646
}
4747
`,
4848
},
49+
// ===========================================
50+
// Tests for mayContainReactCode heuristic
51+
// Files that should be SKIPPED (no React-like function names)
52+
// These contain code that WOULD trigger errors if compiled,
53+
// but since the heuristic skips them, no errors are reported.
54+
// ===========================================
55+
{
56+
name: '[Heuristic] Skips files with only lowercase utility functions',
57+
filename: 'utils.ts',
58+
// This mutates an argument, which would be flagged in a component/hook,
59+
// but this file is skipped because there are no React-like function names
60+
code: normalizeIndent`
61+
function helper(obj) {
62+
obj.key = 'value';
63+
return obj;
64+
}
65+
`,
66+
},
67+
{
68+
name: '[Heuristic] Skips lowercase arrow functions even with mutations',
69+
filename: 'helpers.ts',
70+
// Would be flagged if compiled, but skipped due to lowercase name
71+
code: normalizeIndent`
72+
const processData = (input) => {
73+
input.modified = true;
74+
return input;
75+
};
76+
`,
77+
},
4978
],
5079
invalid: [
5180
{
@@ -68,6 +97,101 @@ const tests: CompilerTestCases = {
6897
},
6998
],
7099
},
100+
// ===========================================
101+
// Tests for mayContainReactCode heuristic
102+
// Files that SHOULD be compiled (have React-like function names)
103+
// These contain violations to prove compilation happens.
104+
// ===========================================
105+
{
106+
name: '[Heuristic] Compiles PascalCase function declaration - detects prop mutation',
107+
filename: 'component.tsx',
108+
code: normalizeIndent`
109+
function MyComponent({a}) {
110+
a.key = 'value';
111+
return <div />;
112+
}
113+
`,
114+
errors: [
115+
{
116+
message: /Modifying component props/,
117+
},
118+
],
119+
},
120+
{
121+
name: '[Heuristic] Compiles PascalCase arrow function - detects prop mutation',
122+
filename: 'component.tsx',
123+
code: normalizeIndent`
124+
const MyComponent = ({a}) => {
125+
a.key = 'value';
126+
return <div />;
127+
};
128+
`,
129+
errors: [
130+
{
131+
message: /Modifying component props/,
132+
},
133+
],
134+
},
135+
{
136+
name: '[Heuristic] Compiles PascalCase function expression - detects prop mutation',
137+
filename: 'component.tsx',
138+
code: normalizeIndent`
139+
const MyComponent = function({a}) {
140+
a.key = 'value';
141+
return <div />;
142+
};
143+
`,
144+
errors: [
145+
{
146+
message: /Modifying component props/,
147+
},
148+
],
149+
},
150+
{
151+
name: '[Heuristic] Compiles exported function declaration - detects prop mutation',
152+
filename: 'component.tsx',
153+
code: normalizeIndent`
154+
export function MyComponent({a}) {
155+
a.key = 'value';
156+
return <div />;
157+
}
158+
`,
159+
errors: [
160+
{
161+
message: /Modifying component props/,
162+
},
163+
],
164+
},
165+
{
166+
name: '[Heuristic] Compiles exported arrow function - detects prop mutation',
167+
filename: 'component.tsx',
168+
code: normalizeIndent`
169+
export const MyComponent = ({a}) => {
170+
a.key = 'value';
171+
return <div />;
172+
};
173+
`,
174+
errors: [
175+
{
176+
message: /Modifying component props/,
177+
},
178+
],
179+
},
180+
{
181+
name: '[Heuristic] Compiles default exported function - detects prop mutation',
182+
filename: 'component.tsx',
183+
code: normalizeIndent`
184+
export default function MyComponent({a}) {
185+
a.key = 'value';
186+
return <div />;
187+
}
188+
`,
189+
errors: [
190+
{
191+
message: /Modifying component props/,
192+
},
193+
],
194+
},
71195
],
72196
};
73197

packages/eslint-plugin-react-hooks/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ process.env.NODE_ENV = 'development';
55
module.exports = {
66
setupFiles: [require.resolve('../../scripts/jest/setupEnvironment.js')],
77
moduleFileExtensions: ['ts', 'js', 'json'],
8+
moduleNameMapper: {
9+
'^babel-plugin-react-compiler$':
10+
'<rootDir>/../../compiler/packages/babel-plugin-react-compiler/dist/index.js',
11+
},
812
};

0 commit comments

Comments
 (0)