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.
This commit is contained in:
Joe Savona
2024-01-29 16:58:49 -05:00
parent 742a6f09bf
commit bc145f6f1f
6 changed files with 78 additions and 15 deletions
@@ -71,11 +71,20 @@ export function filterEslintSuppressionsThatAffectFunction(
}
export function findProgramEslintSuppressions(
programComments: Array<t.Comment>
programComments: Array<t.Comment>,
ruleNames: Array<string>
): Array<EslintSuppressionRange> {
const suppressionRanges: Array<EslintSuppressionRange> = [];
let disableComment: t.Comment | null = null;
let enableComment: t.Comment | null = null;
const rulePattern = `(${ruleNames.join("|")})`;
const disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`
);
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
for (const comment of programComments) {
if (comment.start == null || comment.end == null) {
continue;
@@ -87,27 +96,17 @@ export function findProgramEslintSuppressions(
* CommentLine within the block.
*/
disableComment == null &&
/eslint-disable-next-line react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
comment.value
)
disableNextLinePattern.test(comment.value)
) {
disableComment = comment;
enableComment = comment;
}
if (
/eslint-disable react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
comment.value
)
) {
if (disablePattern.test(comment.value)) {
disableComment = comment;
}
if (
/eslint-enable react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
comment.value
)
) {
if (enablePattern.test(comment.value)) {
enableComment = comment;
}
@@ -91,6 +91,17 @@ export type PluginOptions = {
* ```
*/
enableUseMemoCachePolyfill: boolean;
/**
* By default React Compiler will skip compilation of code that suppresses the default
* React ESLint rules, since this is a strong indication that the code may be breaking React rules
* in some way.
*
* Use eslintSuppressionRules to pass a custom set of rule names: any code which suppresses the
* provided rules will skip compilation. To disable this feature (never bailout of compilation
* even if the default ESLint is suppressed), pass an empty array.
*/
eslintSuppressionRules?: Array<string> | null | undefined;
};
const CompilationModeSchema = z.enum([
@@ -157,6 +168,7 @@ export const defaultOptions: PluginOptions = {
gating: null,
noEmit: false,
enableUseMemoCachePolyfill: false,
eslintSuppressionRules: null,
} as const;
export function parsePluginOptions(obj: unknown): PluginOptions {
@@ -177,6 +177,11 @@ export function createNewFunctionNode(
*/
const ALREADY_COMPILED: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
const DEFAULT_ESLINT_SUPPRESSIONS = [
"react-hooks/exhaustive-deps",
"react-hooks/rules-of-hooks",
];
export function compileProgram(
program: NodePath<t.Program>,
pass: CompilerPass
@@ -189,7 +194,10 @@ export function compileProgram(
* we may still need to run Forget's analysis on every function (even if we
* have already encountered errors) for reporting.
*/
const eslintSuppressions = findProgramEslintSuppressions(pass.comments);
const eslintSuppressions = findProgramEslintSuppressions(
pass.comments,
options.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS
);
const lintError = suppressionsToCompilerError(eslintSuppressions);
let hasCriticalError = lintError != null;
const compiledFns: CompileResult[] = [];
@@ -0,0 +1,27 @@
## Input
```javascript
// @eslintSuppressionRules(my-app/react-rule)
/* eslint-disable my-app/react-rule */
function lowercasecomponent() {
"use forget";
const x = [];
// eslint-disable-next-line my-app/react-rule
return <div>{x}</div>;
}
/* eslint-enable my-app/react-rule */
```
## Error
```
[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)
[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)
```
@@ -0,0 +1,10 @@
// @eslintSuppressionRules(my-app/react-rule)
/* eslint-disable my-app/react-rule */
function lowercasecomponent() {
"use forget";
const x = [];
// eslint-disable-next-line my-app/react-rule
return <div>{x}</div>;
}
/* eslint-enable my-app/react-rule */
@@ -84,6 +84,12 @@ export function transformFixtureInput(
panicThreshold = "NONE";
}
let eslintSuppressionRules: Array<string> | null = null;
const match = /@eslintSuppressionRules\(([^)]+)\)/.exec(firstLine);
if (match != null) {
eslintSuppressionRules = match[1].split("|");
}
const config = parseConfigPragmaFn(firstLine);
const result = pluginFn(
input,
@@ -132,6 +138,7 @@ export function transformFixtureInput(
panicThreshold,
noEmit: false,
enableUseMemoCachePolyfill,
eslintSuppressionRules,
},
includeAst
);