Option to bail on Flow react-rule suppressions
Mike Vitousek committed
Feb 9, 2024 at 14:21 UTC
19476aa5f6f448a97733dc95739b4873a8f8ab12
9 files changed
+152
-33
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Options.ts
+3
@@ -102,6 +102,8 @@ export type PluginOptions = {
102
* even if the default ESLint is suppressed), pass an empty array.
103
*/
104
eslintSuppressionRules?: Array<string> | null | undefined;
105
+
106
+ flowSuppressions: boolean;
107
};
108
109
const CompilationModeSchema = z.enum([
@@ -169,6 +171,7 @@ export const defaultOptions: PluginOptions = {
171
noEmit: false,
172
enableUseMemoCachePolyfill: false,
173
eslintSuppressionRules: null,
174
+ flowSuppressions: false,
175
} as const;
176
177
export function parsePluginOptions(obj: unknown): PluginOptions {
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts
+12
-11
@@ -20,15 +20,15 @@ import {
20
import { CodegenFunction } from "../ReactiveScopes";
21
import { isComponentDeclaration } from "../Utils/ComponentDeclaration";
22
import { assertExhaustive } from "../Utils/utils";
23
-import {
24
- filterEslintSuppressionsThatAffectFunction,
25
- findProgramEslintSuppressions,
26
- suppressionsToCompilerError,
27
-} from "./EslintSuppression";
23
import { insertGatedFunctionDeclaration } from "./Gating";
24
import { addImportsToProgram, updateUseMemoCacheImport } from "./Imports";
25
import { PluginOptions, parsePluginOptions } from "./Options";
26
import { compileFn } from "./Pipeline";
27
+import {
28
+ filterSuppressionsThatAffectFunction,
29
+ findProgramSuppressions,
30
+ suppressionsToCompilerError,
31
+} from "./Suppression";
32
33
export type CompilerPass = {
34
opts: PluginOptions;
@@ -196,11 +196,12 @@ export function compileProgram(
196
* we may still need to run Forget's analysis on every function (even if we
197
* have already encountered errors) for reporting.
198
*/
199
- const eslintSuppressions = findProgramEslintSuppressions(
199
+ const suppressions = findProgramSuppressions(
200
pass.comments,
201
- options.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS
201
+ options.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
202
+ options.flowSuppressions,
203
);
203
- const lintError = suppressionsToCompilerError(eslintSuppressions);
204
+ const lintError = suppressionsToCompilerError(suppressions);
205
let hasCriticalError = lintError != null;
206
const compiledFns: CompileResult[] = [];
207
@@ -223,9 +224,9 @@ export function compileProgram(
224
* Program node itself. We need to figure out whether an eslint suppression range
225
* applies to this function first.
226
*/
226
- const eslintSuppressionsInFunction =
227
- filterEslintSuppressionsThatAffectFunction(eslintSuppressions, fn);
228
- if (eslintSuppressionsInFunction.length > 0) {
227
+ const suppressionsInFunction =
228
+ filterSuppressionsThatAffectFunction(suppressions, fn);
229
+ if (suppressionsInFunction.length > 0) {
230
handleError(lintError, pass, fn.node.loc ?? null);
231
}
232
}
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Suppression.ts
renamed
+50
-18
@@ -16,26 +16,31 @@ import {
16
17
/**
18
* Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. In the
19
- * case of a CommentLine, both the disable and enable point to the same comment.
19
+ * case of a CommentLine or a relevant Flow suppression, both the disable and enable point to the
20
+ * same comment.
21
*
22
* The enable comment can be missing in the case where only a disable block is present, ie the rest
23
* of the file has potential React violations.
24
*/
24
-export type EslintSuppressionRange = {
25
+export type SuppressionRange = {
26
disableComment: t.Comment;
27
enableComment: t.Comment | null;
28
+ source: SuppressionSource;
29
};
30
31
+type SuppressionSource =
32
+ 'Eslint' | 'Flow'
33
+
34
/**
30
- * An eslint suppression affects a function if:
35
+ * An suppression affects a function if:
36
* 1. The suppression is within the function's body; or
37
* 2. The suppression wraps the function
38
*/
34
-export function filterEslintSuppressionsThatAffectFunction(
35
- suppressionRanges: Array<EslintSuppressionRange>,
39
+export function filterSuppressionsThatAffectFunction(
40
+ suppressionRanges: Array<SuppressionRange>,
41
fn: NodePath<t.Function>
37
-): Array<EslintSuppressionRange> {
38
- const suppressionsInScope: Array<EslintSuppressionRange> = [];
42
+): Array<SuppressionRange> {
43
+ const suppressionsInScope: Array<SuppressionRange> = [];
44
const fnNode = fn.node;
45
for (const suppressionRange of suppressionRanges) {
46
if (
@@ -70,13 +75,15 @@ export function filterEslintSuppressionsThatAffectFunction(
75
return suppressionsInScope;
76
}
77
73
-export function findProgramEslintSuppressions(
78
+export function findProgramSuppressions(
79
programComments: Array<t.Comment>,
75
- ruleNames: Array<string>
76
-): Array<EslintSuppressionRange> {
77
- const suppressionRanges: Array<EslintSuppressionRange> = [];
80
+ ruleNames: Array<string>,
81
+ flowSuppressions: boolean,
82
+): Array<SuppressionRange> {
83
+ const suppressionRanges: Array<SuppressionRange> = [];
84
let disableComment: t.Comment | null = null;
85
let enableComment: t.Comment | null = null;
86
+ let source: SuppressionSource | null = null;
87
88
const rulePattern = `(${ruleNames.join("|")})`;
89
const disableNextLinePattern = new RegExp(
@@ -84,6 +91,9 @@ export function findProgramEslintSuppressions(
91
);
92
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
93
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
94
+ const flowSuppressionPattern = new RegExp(
95
+ '\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule'
96
+ );
97
98
for (const comment of programComments) {
99
if (comment.start == null || comment.end == null) {
@@ -100,36 +110,48 @@ export function findProgramEslintSuppressions(
110
) {
111
disableComment = comment;
112
enableComment = comment;
113
+ source = 'Eslint';
114
+ }
115
+
116
+ if (
117
+ flowSuppressions &&
118
+ disableComment == null &&
119
+ flowSuppressionPattern.test(comment.value)
120
+ ) {
121
+ disableComment = comment;
122
+ enableComment = comment;
123
+ source = 'Flow';
124
}
125
126
if (disablePattern.test(comment.value)) {
127
disableComment = comment;
128
+ source = 'Eslint';
129
}
130
109
- if (enablePattern.test(comment.value)) {
131
+ if (enablePattern.test(comment.value) && source === 'Eslint') {
132
enableComment = comment;
133
}
134
113
- if (disableComment != null) {
135
+ if (disableComment != null && source != null) {
136
suppressionRanges.push({
137
disableComment: disableComment,
138
enableComment: enableComment,
139
+ source,
140
});
141
disableComment = null;
142
enableComment = null;
143
+ source = null;
144
}
145
}
146
return suppressionRanges;
147
}
148
149
export function suppressionsToCompilerError(
126
- suppressionRanges: Array<EslintSuppressionRange>
150
+ suppressionRanges: Array<SuppressionRange>
151
): CompilerError | null {
152
if (suppressionRanges.length === 0) {
153
return null;
154
}
131
- const reason =
132
- "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";
155
const error = new CompilerError();
156
for (const suppressionRange of suppressionRanges) {
157
if (
@@ -138,15 +160,25 @@ export function suppressionsToCompilerError(
160
) {
161
continue;
162
}
163
+ let reason, suggestion;
164
+ switch (suppressionRange.source) {
165
+ case 'Eslint':
166
+ reason = "React Forget has bailed out of optimizing this component as one or more React eslint rules were disabled";
167
+ suggestion = "Remove the eslint disable";
168
+ break;
169
+ case 'Flow':
170
+ reason = "React Forget has bailed out of optimizing this component as one or more React rule violations were reported by Flow";
171
+ suggestion = "Remove the Flow suppression and address the React error";
172
+ }
173
error.pushErrorDetail(
174
new CompilerErrorDetail({
143
- reason,
175
+ reason: `${reason}. React Forget only works when your components follow all the rules of React, disabling them may result in undefined behavior`,
176
description: suppressionRange.disableComment.value.trim(),
177
severity: ErrorSeverity.InvalidReact,
178
loc: suppressionRange.disableComment.loc ?? null,
179
suggestions: [
180
{
149
- description: "Remove the eslint disable",
181
+ description: suggestion,
182
range: [
183
suppressionRange.disableComment.start,
184
suppressionRange.disableComment.end,
compiler/packages/babel-plugin-react-forget/src/Entrypoint/index.ts
+1
-1
@@ -5,9 +5,9 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-export * from "./EslintSuppression";
8
export * from "./Gating";
9
export * from "./Imports";
10
export * from "./Options";
11
export * from "./Pipeline";
12
export * from "./Program";
13
+export * from "./Suppression";
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md
new
+22
@@ -0,0 +1,22 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableFlowSuppressions
6
+
7
+function Foo(props) {
8
+ // $FlowFixMe[react-rule-hook]
9
+ useX();
10
+ return null;
11
+}
12
+
13
+```
14
+
15
+
16
+## Error
17
+
18
+```
19
+[ReactForget] InvalidReact: React Forget has bailed out of optimizing this component as one or more React rule violations were reported by Flow. React Forget only works when your components follow all the rules of React, disabling them may result in undefined behavior. $FlowFixMe[react-rule-hook] (4:4)
20
+```
21
+
22
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.js
new
+7
@@ -0,0 +1,7 @@
1
+// @enableFlowSuppressions
2
+
3
+function Foo(props) {
4
+ // $FlowFixMe[react-rule-hook]
5
+ useX();
6
+ return null;
7
+}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/no-flow-bailout-unrelated.expect.md
new
+39
@@ -0,0 +1,39 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableFlowSuppressions
6
+
7
+function Foo(props) {
8
+ // $FlowFixMe[incompatible-type]
9
+ useX();
10
+ const x = new Foo(...props.foo, null, ...[props.bar]);
11
+ return x;
12
+}
13
+
14
+```
15
+
16
+## Code
17
+
18
+```javascript
19
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableFlowSuppressions
20
+
21
+function Foo(props) {
22
+ const $ = useMemoCache(3);
23
+
24
+ useX();
25
+ let t0;
26
+ if ($[0] !== props.bar || $[1] !== props.foo) {
27
+ t0 = new Foo(...props.foo, null, ...[props.bar]);
28
+ $[0] = props.bar;
29
+ $[1] = props.foo;
30
+ $[2] = t0;
31
+ } else {
32
+ t0 = $[2];
33
+ }
34
+ const x = t0;
35
+ return x;
36
+}
37
+
38
+```
39
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/no-flow-bailout-unrelated.js
new
+8
@@ -0,0 +1,8 @@
1
+// @enableFlowSuppressions
2
+
3
+function Foo(props) {
4
+ // $FlowFixMe[incompatible-type]
5
+ useX();
6
+ const x = new Foo(...props.foo, null, ...[props.bar]);
7
+ return x;
8
+}
compiler/packages/fixture-test-utils/src/compiler-utils.ts
+10
-3
@@ -93,6 +93,12 @@ export function transformFixtureInput(
93
eslintSuppressionRules = eslintSuppressionMatch[1].split("|");
94
}
95
96
+ let flowSuppressions: boolean = false;
97
+ if (firstLine.includes("@enableFlowSuppressions")) {
98
+ flowSuppressions = true;
99
+ }
100
+
101
+
102
const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
103
if (
104
hookPatternMatch &&
@@ -156,6 +162,7 @@ export function transformFixtureInput(
162
noEmit: false,
163
enableUseMemoCachePolyfill,
164
eslintSuppressionRules,
165
+ flowSuppressions,
166
},
167
includeAst
168
);
@@ -165,9 +172,9 @@ export function transformFixtureInput(
172
code:
173
result.code != null
174
? prettier.format(result.code, {
168
- semi: true,
169
- parser: language === "typescript" ? "babel-ts" : "flow",
170
- })
175
+ semi: true,
176
+ parser: language === "typescript" ? "babel-ts" : "flow",
177
+ })
178
: result.code,
179
};
180
}