@samitouri / QOS-React-1 / commits / bc145f6f1f

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 );