diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts index c87bcadbe98bc..0cd28702ae8b7 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts @@ -199,6 +199,44 @@ const tests: CompilerTestCases = { }, ], }, + { + name: "'use no forget' does not disable eslint rule", + code: normalizeIndent` + let count = 0; + function Component() { + 'use no forget'; + count = count + 1; + return
Hello world {count}
+ } + `, + errors: [ + { + message: + 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', + }, + ], + }, + { + name: "Unused 'use no forget' directive is reported when no errors are present", + code: normalizeIndent` + function Component() { + 'use no forget'; + return
Hello world
+ } + `, + errors: [ + { + message: "Unused 'use no forget' directive", + suggestions: [ + { + output: + // yuck + '\nfunction Component() {\n \n return
Hello world
\n}\n', + }, + ], + }, + ], + }, ], }; diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index 2fe34a0b7ff2b..27a600fe0cab5 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -15,10 +15,12 @@ import BabelPluginReactCompiler, { ErrorSeverity, parsePluginOptions, validateEnvironmentConfig, + OPT_OUT_DIRECTIVES, type PluginOptions, } from 'babel-plugin-react-compiler/src'; import {Logger} from 'babel-plugin-react-compiler/src/Entrypoint'; import type {Rule} from 'eslint'; +import {Statement} from 'estree'; import * as HermesParser from 'hermes-parser'; type CompilerErrorDetailWithLoc = Omit & { @@ -146,6 +148,7 @@ const rule: Rule.RuleModule = { userOpts['__unstable_donotuse_reportAllBailouts']; } + let shouldReportUnusedOptOutDirective = true; const options: PluginOptions = { ...parsePluginOptions(userOpts), ...COMPILER_OPTIONS, @@ -155,6 +158,7 @@ const rule: Rule.RuleModule = { logEvent: (filename, event): void => { userLogger?.logEvent(filename, event); if (event.kind === 'CompileError') { + shouldReportUnusedOptOutDirective = false; const detail = event.detail; const suggest = makeSuggestions(detail); if (__unstable_donotuse_reportAllBailouts && event.fnLoc != null) { @@ -269,7 +273,52 @@ const rule: Rule.RuleModule = { /* errors handled by injected logger */ } } - return {}; + + function reportUnusedOptOutDirective(stmt: Statement) { + if ( + stmt.type === 'ExpressionStatement' && + stmt.expression.type === 'Literal' && + typeof stmt.expression.value === 'string' && + OPT_OUT_DIRECTIVES.has(stmt.expression.value) && + stmt.loc != null + ) { + context.report({ + message: `Unused '${stmt.expression.value}' directive`, + loc: stmt.loc, + suggest: [ + { + desc: 'Remove the directive', + fix(fixer) { + return fixer.remove(stmt); + }, + }, + ], + }); + } + } + if (shouldReportUnusedOptOutDirective) { + return { + FunctionDeclaration(fnDecl) { + for (const stmt of fnDecl.body.body) { + reportUnusedOptOutDirective(stmt); + } + }, + ArrowFunctionExpression(fnExpr) { + if (fnExpr.body.type === 'BlockStatement') { + for (const stmt of fnExpr.body.body) { + reportUnusedOptOutDirective(stmt); + } + } + }, + FunctionExpression(fnExpr) { + for (const stmt of fnExpr.body.body) { + reportUnusedOptOutDirective(stmt); + } + }, + }; + } else { + return {}; + } }, };