Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[eslint-plugin-react-hooks] Skip compilation for non-React files (#35… · react/react@03ee29d · GitHub
Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [eslint-plugin-react-hooks] Skip compilation for non-React files (#35… · react/react@03ee29d · GitHub
Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [eslint-plugin-react-hooks] Skip compilation for non-React files (#35… · react/react@03ee29d · GitHub
Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [eslint-plugin-react-hooks] Skip compilation for non-React files (#35… · react/react@03ee29d · GitHub
Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [eslint-plugin-react-hooks] Skip compilation for non-React files (#35… · react/react@03ee29d · GitHub
Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [eslint-plugin-react-hooks] Skip compilation for non-React files (#35… · react/react@03ee29d · GitHub
Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [eslint-plugin-react-hooks] Skip compilation for non-React files (#35… · react/react@03ee29d · GitHub
Skip to content

Commit 03ee29d

Browse files
authored
[eslint-plugin-react-hooks] Skip compilation for non-React files (#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

4 files changed

Lines changed: 390 additions & 0 deletions

File tree

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+
constESLintTesterV8=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+
functionnormalizeIndent(strings: TemplateStringsArray): string{
18+
constcodeLines=strings[0]?.split('\n')??[];
19+
constleftPadding=codeLines[1]?.match(/\s+/)![0]??'';
20+
returncodeLines.map(line=>line.slice(leftPadding.length)).join('\n');
21+
}
22+
23+
typeCompilerTestCases={
24+
valid: RuleTester.ValidTestCase[];
25+
invalid: RuleTester.InvalidTestCase[];
26+
};
27+
28+
consttests: 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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentpropsorhookarguments/,
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: /Modifyingcomponentpropsorhookarguments/,
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
consteslintTester=newESLintTesterV8({
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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: /Modifyingcomponentprops/,
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)