Repository: facebook/react
Pull request: [compiler] Setup RecommendedLatest preset


Renames the `recommended` property on LintRule to `preset`, to allow exporting rules for different presets. For now the `Recommended` and `RecommendedLatest` presets are the same, but in the next PR I will enable more rules for the latest preset.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34782).
* #34783
* __->__ #34782

Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts
patch: @@ -669,6 +669,21 @@ export enum ErrorCategory {
   FBT = 'FBT',
 }
 
+export enum LintRulePreset {
+  /**
+   * Rules that are stable and included in the `recommended` preset.
+   */
+  Recommended = 'recommended',
+  /**
+   * Rules that are more experimental and only included in the `recommended-latest` preset.
+   */
+  RecommendedLatest = 'recommended-latest',
+  /**
+   * Rules that are disabled.
+   */
+  Off = 'off',
+}
+
 export type LintRule = {
   // Stores the category the rule corresponds to, used to filter errors when reporting
   category: ErrorCategory;
@@ -689,15 +704,14 @@ export type LintRule = {
   description: string;
 
   /**
-   * If true, this rule will automatically appear in the default, "recommended" ESLint
-   * rule set. Otherwise it will be part of an `allRules` export that developers can
-   * use to opt-in to showing output of all possible rules.
+   * Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
+   * any preset.
    *
    * NOTE: not all validations are enabled by default! Setting this flag only affects
    * whether a given rule is part of the recommended set. The corresponding validation
    * also should be enabled by default if you want the error to actually show up!
    */
-  recommended: boolean;
+  preset: LintRulePreset;
 };
 
 const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
@@ -720,7 +734,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'automatic-effect-dependencies',
         description:
           'Verifies that automatic effect dependencies are compiled if opted-in',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.CapitalizedCalls: {
@@ -730,7 +744,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'capitalized-calls',
         description:
           'Validates against calling capitalized functions/methods instead of using JSX',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.Config: {
@@ -739,7 +753,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Error,
         name: 'config',
         description: 'Validates the compiler configuration options',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.EffectDependencies: {
@@ -748,7 +762,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Error,
         name: 'memoized-effect-dependencies',
         description: 'Validates that effect dependencies are memoized',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.EffectDerivationsOfState: {
@@ -758,7 +772,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'no-deriving-state-in-effects',
         description:
           'Validates against deriving values from state in an effect',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.EffectSetState: {
@@ -768,7 +782,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'set-state-in-effect',
         description:
           'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.ErrorBoundaries: {
@@ -778,7 +792,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'error-boundaries',
         description:
           'Validates usage of error boundaries instead of try/catch for errors in child components',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.Factories: {
@@ -789,7 +803,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         description:
           'Validates against higher order functions defining nested components or hooks. ' +
           'Components and hooks should be defined at the module level',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.FBT: {
@@ -798,7 +812,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Error,
         name: 'fbt',
         description: 'Validates usage of fbt',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.Fire: {
@@ -807,7 +821,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Error,
         name: 'fire',
         description: 'Validates usage of `fire`',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.Gating: {
@@ -817,7 +831,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'gating',
         description:
           'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.Globals: {
@@ -828,7 +842,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         description:
           'Validates against assignment/mutation of globals during render, part of ensuring that ' +
           '[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.Hooks: {
@@ -842,7 +856,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
          * We need to dedeupe these (moving the remaining bits into the compiler) and then enable
          * this rule.
          */
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.Immutability: {
@@ -852,7 +866,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'immutability',
         description:
           'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.Invariant: {
@@ -861,7 +875,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Error,
         name: 'invariant',
         description: 'Internal invariants',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.PreserveManualMemo: {
@@ -873,7 +887,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
           'Validates that existing manual memoized is preserved by the compiler. ' +
           'React Compiler will only compile components and hooks if its inference ' +
           '[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.Purity: {
@@ -883,7 +897,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'purity',
         description:
           'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.Refs: {
@@ -893,7 +907,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'refs',
         description:
           'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.RenderSetState: {
@@ -903,7 +917,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'set-state-in-render',
         description:
           'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.StaticComponents: {
@@ -913,7 +927,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'static-components',
         description:
           'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.Suppression: {
@@ -922,7 +936,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Error,
         name: 'rule-suppression',
         description: 'Validates against suppression of other rules',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.Syntax: {
@@ -931,7 +945,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Error,
         name: 'syntax',
         description: 'Validates against invalid syntax',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.Todo: {
@@ -940,7 +954,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         severity: ErrorSeverity.Hint,
         name: 'todo',
         description: 'Unimplemented features',
-        recommended: false,
+        preset: LintRulePreset.Off,
       };
     }
     case ErrorCategory.UnsupportedSyntax: {
@@ -950,7 +964,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'unsupported-syntax',
         description:
           'Validates against syntax that we do not plan to support in React Compiler',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.UseMemo: {
@@ -960,7 +974,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'use-memo',
         description:
           'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     case ErrorCategory.IncompatibleLibrary: {
@@ -970,7 +984,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
         name: 'incompatible-library',
         description:
           'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
-        recommended: true,
+        preset: LintRulePreset.Recommended,
       };
     }
     default: {

filename: compiler/packages/babel-plugin-react-compiler/src/index.ts
patch: @@ -14,6 +14,7 @@ export {
   ErrorSeverity,
   ErrorCategory,
   LintRules,
+  LintRulePreset,
   type CompilerErrorDetailOptions,
   type CompilerDiagnosticOptions,
   type CompilerDiagnosticDetail,

filename: compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts
patch: @@ -15,6 +15,7 @@ import type {Linter, Rule} from 'eslint';
 import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
 import {
   ErrorSeverity,
+  LintRulePreset,
   LintRules,
   type LintRule,
 } from 'babel-plugin-react-compiler/src/CompilerError';
@@ -150,7 +151,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
       type: 'problem',
       docs: {
         description: rule.description,
-        recommended: rule.recommended,
+        recommended: rule.preset === LintRulePreset.Recommended,
       },
       fixable: 'code',
       hasSuggestions: true,
@@ -171,7 +172,16 @@ export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
 }, {} as RulesConfig);
 
 export const recommendedRules: RulesConfig = LintRules.filter(
-  rule => rule.recommended,
+  rule => rule.preset === LintRulePreset.Recommended,
+).reduce((acc, rule) => {
+  acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
+  return acc;
+}, {} as RulesConfig);
+
+export const recommendedLatestRules: RulesConfig = LintRules.filter(
+  rule =>
+    rule.preset === LintRulePreset.Recommended ||
+    rule.preset === LintRulePreset.RecommendedLatest,
 ).reduce((acc, rule) => {
   acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
   return acc;

filename: packages/eslint-plugin-react-hooks/src/index.ts
patch: @@ -11,6 +11,7 @@ import {
   allRules,
   mapErrorSeverityToESlint,
   recommendedRules,
+  recommendedLatestRules,
 } from './shared/ReactCompiler';
 import RulesOfHooks from './rules/RulesOfHooks';
 
@@ -27,7 +28,7 @@ const basicRuleConfigs = {
   'react-hooks/exhaustive-deps': 'warn',
 } as const satisfies Linter.RulesRecord;
 
-const compilerRuleConfigs = Object.fromEntries(
+const recommendedCompilerRuleConfigs = Object.fromEntries(
   Object.entries(recommendedRules).map(([name, ruleConfig]) => {
     return [
       `react-hooks/${name}` as const,
@@ -36,9 +37,22 @@ const compilerRuleConfigs = Object.fromEntries(
   }),
 ) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
 
-const allRuleConfigs: Linter.RulesRecord = {
+const recommendedLatestCompilerRuleConfigs = Object.fromEntries(
+  Object.entries(recommendedLatestRules).map(([name, ruleConfig]) => {
+    return [
+      `react-hooks/${name}` as const,
+      mapErrorSeverityToESlint(ruleConfig.severity),
+    ] as const;
+  }),
+) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
+
+const recommendedRuleConfigs: Linter.RulesRecord = {
+  ...basicRuleConfigs,
+  ...recommendedCompilerRuleConfigs,
+};
+const recommendedLatestRuleConfigs: Linter.RulesRecord = {
   ...basicRuleConfigs,
-  ...compilerRuleConfigs,
+  ...recommendedLatestCompilerRuleConfigs,
 };
 
 const plugins = ['react-hooks'];
@@ -51,11 +65,11 @@ type ReactHooksFlatConfig = {
 const configs = {
   recommended: {
     plugins,
-    rules: allRuleConfigs,
+    rules: recommendedRuleConfigs,
   },
   'recommended-latest': {
     plugins,
-    rules: allRuleConfigs,
+    rules: recommendedLatestRuleConfigs,
   },
   flat: {} as Record<string, ReactHooksFlatConfig>,
 };

filename: packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts
patch: @@ -14,6 +14,7 @@ import {
   LintRules,
   type LintRule,
   ErrorSeverity,
+  LintRulePreset,
 } from 'babel-plugin-react-compiler';
 import {type Linter, type Rule} from 'eslint';
 import runReactCompiler, {RunCacheEntry} from './RunReactCompiler';
@@ -149,7 +150,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
       type: 'problem',
       docs: {
         description: rule.description,
-        recommended: rule.recommended,
+        recommended: rule.preset === LintRulePreset.Recommended,
       },
       fixable: 'code',
       hasSuggestions: true,
@@ -164,23 +165,26 @@ type RulesConfig = {
   [name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
 };
 
-export const allRules: RulesConfig = LintRules.reduce(
-  (acc, rule) => {
-    acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
-    return acc;
-  },
-  {} as RulesConfig,
-);
+export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
+  acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
+  return acc;
+}, {} as RulesConfig);
 
 export const recommendedRules: RulesConfig = LintRules.filter(
-  rule => rule.recommended,
-).reduce(
-  (acc, rule) => {
-    acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
-    return acc;
-  },
-  {} as RulesConfig,
-);
+  rule => rule.preset === LintRulePreset.Recommended,
+).reduce((acc, rule) => {
+  acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
+  return acc;
+}, {} as RulesConfig);
+
+export const recommendedLatestRules: RulesConfig = LintRules.filter(
+  rule =>
+    rule.preset === LintRulePreset.Recommended ||
+    rule.preset === LintRulePreset.RecommendedLatest,
+).reduce((acc, rule) => {
+  acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
+  return acc;
+}, {} as RulesConfig);
 
 export function mapErrorSeverityToESlint(
   severity: ErrorSeverity,