diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index 2e7acac2542..0db911dbe56 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts @@ -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: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index c5d8c4cb6e8..d2abd744d6b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -14,6 +14,7 @@ export { ErrorSeverity, ErrorCategory, LintRules, + LintRulePreset, type CompilerErrorDetailOptions, type CompilerDiagnosticOptions, type CompilerDiagnosticDetail, 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 7dacbe6d190..8f41b3afaba 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -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; diff --git a/packages/eslint-plugin-react-hooks/src/index.ts b/packages/eslint-plugin-react-hooks/src/index.ts index 2de0141a8e5..6d72c1daa57 100644 --- a/packages/eslint-plugin-react-hooks/src/index.ts +++ b/packages/eslint-plugin-react-hooks/src/index.ts @@ -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, }; diff --git a/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts index cdb3af38489..854e26149f7 100644 --- a/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts @@ -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,