Support customizable eslint suppressions
The compiler bails out of compiling code that contains suppressions of the official React ESLint rules. However, some apps may use additional rules that they want to trigger bailouts for, or use the official rules under a different name (we do this at Meta). This PR adds a compiler flag to specify a custom set of line rule names, suppression of which should trigger a bailout.
Joe Savona committed
Jan 29, 2024 at 16:58 UTC
bc145f6f1fb469a7e09608c1714d074734620871
6 files changed
+78
-15
compiler/packages/babel-plugin-react-forget/src/Entrypoint/EslintSuppression.ts
+13
-14
@@ -71,11 +71,20 @@ export function filterEslintSuppressionsThatAffectFunction(
71
}
72
73
export function findProgramEslintSuppressions(
74
- programComments: Array<t.Comment>
74
+ programComments: Array<t.Comment>,
75
+ ruleNames: Array<string>
76
): Array<EslintSuppressionRange> {
77
const suppressionRanges: Array<EslintSuppressionRange> = [];
78
let disableComment: t.Comment | null = null;
79
let enableComment: t.Comment | null = null;
80
+
81
+ const rulePattern = `(${ruleNames.join("|")})`;
82
+ const disableNextLinePattern = new RegExp(
83
+ `eslint-disable-next-line ${rulePattern}`
84
+ );
85
+ const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
86
+ const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
87
+
88
for (const comment of programComments) {
89
if (comment.start == null || comment.end == null) {
90
continue;
@@ -87,27 +96,17 @@ export function findProgramEslintSuppressions(
96
* CommentLine within the block.
97
*/
98
disableComment == null &&
90
- /eslint-disable-next-line react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
91
- comment.value
92
- )
99
+ disableNextLinePattern.test(comment.value)
100
) {
101
disableComment = comment;
102
enableComment = comment;
103
}
104
98
- if (
99
- /eslint-disable react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
100
- comment.value
101
- )
102
- ) {
105
+ if (disablePattern.test(comment.value)) {
106
disableComment = comment;
107
}
108
106
- if (
107
- /eslint-enable react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
108
- comment.value
109
- )
110
- ) {
109
+ if (enablePattern.test(comment.value)) {
110
enableComment = comment;
111
}
112
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Options.ts
+12
@@ -91,6 +91,17 @@ export type PluginOptions = {
91
* ```
92
*/
93
enableUseMemoCachePolyfill: boolean;
94
+
95
+ /**
96
+ * By default React Compiler will skip compilation of code that suppresses the default
97
+ * React ESLint rules, since this is a strong indication that the code may be breaking React rules
98
+ * in some way.
99
+ *
100
+ * Use eslintSuppressionRules to pass a custom set of rule names: any code which suppresses the
101
+ * provided rules will skip compilation. To disable this feature (never bailout of compilation
102
+ * even if the default ESLint is suppressed), pass an empty array.
103
+ */
104
+ eslintSuppressionRules?: Array<string> | null | undefined;
105
};
106
107
const CompilationModeSchema = z.enum([
@@ -157,6 +168,7 @@ export const defaultOptions: PluginOptions = {
168
gating: null,
169
noEmit: false,
170
enableUseMemoCachePolyfill: false,
171
+ eslintSuppressionRules: null,
172
} as const;
173
174
export function parsePluginOptions(obj: unknown): PluginOptions {
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts
+9
-1
@@ -177,6 +177,11 @@ export function createNewFunctionNode(
177
*/
178
const ALREADY_COMPILED: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
179
180
+const DEFAULT_ESLINT_SUPPRESSIONS = [
181
+ "react-hooks/exhaustive-deps",
182
+ "react-hooks/rules-of-hooks",
183
+];
184
+
185
export function compileProgram(
186
program: NodePath<t.Program>,
187
pass: CompilerPass
@@ -189,7 +194,10 @@ export function compileProgram(
194
* we may still need to run Forget's analysis on every function (even if we
195
* have already encountered errors) for reporting.
196
*/
192
- const eslintSuppressions = findProgramEslintSuppressions(pass.comments);
197
+ const eslintSuppressions = findProgramEslintSuppressions(
198
+ pass.comments,
199
+ options.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS
200
+ );
201
const lintError = suppressionsToCompilerError(eslintSuppressions);
202
let hasCriticalError = lintError != null;
203
const compiledFns: CompileResult[] = [];
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md
new
+27
@@ -0,0 +1,27 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @eslintSuppressionRules(my-app/react-rule)
6
+
7
+/* eslint-disable my-app/react-rule */
8
+function lowercasecomponent() {
9
+ "use forget";
10
+ const x = [];
11
+ // eslint-disable-next-line my-app/react-rule
12
+ return <div>{x}</div>;
13
+}
14
+/* eslint-enable my-app/react-rule */
15
+
16
+```
17
+
18
+
19
+## Error
20
+
21
+```
22
+[ReactForget] InvalidReact: React Forget has bailed out of optimizing this component as one or more React eslint rules were disabled. React Forget only works when your components follow all the rules of React, disabling them may result in undefined behavior. eslint-disable my-app/react-rule (3:3)
23
+
24
+[ReactForget] InvalidReact: React Forget has bailed out of optimizing this component as one or more React eslint rules were disabled. React Forget only works when your components follow all the rules of React, disabling them may result in undefined behavior. eslint-disable-next-line my-app/react-rule (7:7)
25
+```
26
+
27
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.js
new
+10
@@ -0,0 +1,10 @@
1
+// @eslintSuppressionRules(my-app/react-rule)
2
+
3
+/* eslint-disable my-app/react-rule */
4
+function lowercasecomponent() {
5
+ "use forget";
6
+ const x = [];
7
+ // eslint-disable-next-line my-app/react-rule
8
+ return <div>{x}</div>;
9
+}
10
+/* eslint-enable my-app/react-rule */
compiler/packages/fixture-test-utils/src/compiler-utils.ts
+7
@@ -84,6 +84,12 @@ export function transformFixtureInput(
84
panicThreshold = "NONE";
85
}
86
87
+ let eslintSuppressionRules: Array<string> | null = null;
88
+ const match = /@eslintSuppressionRules\(([^)]+)\)/.exec(firstLine);
89
+ if (match != null) {
90
+ eslintSuppressionRules = match[1].split("|");
91
+ }
92
+
93
const config = parseConfigPragmaFn(firstLine);
94
const result = pluginFn(
95
input,
@@ -132,6 +138,7 @@ export function transformFixtureInput(
138
panicThreshold,
139
noEmit: false,
140
enableUseMemoCachePolyfill,
141
+ eslintSuppressionRules,
142
},
143
includeAst
144
);